Roles are an excellent object-oriented tool both for allomorphism and for
reuse.
Roles facilitate allomorphism by favoring "does this object do X" versus "is
this object a subclass of X". You often care more about capability than
inheritance. In a sense, roles encode types better than inheritance.
Roles also provide an excellent faculty for reuse. This effectively eliminates
multiple inheritance, which is often the only solution for sharing code between
unrelated classes.
Roles can combine with conflict detection. This eliminates accidental shadowing
of methods that is painful with multiple inheritance and mixins.
Parameterized roles (via MooseX::Role::Parameterized) improve the reusability
of roles by letting each consumer cater the role to its needs. This does
sacrifice some allomorphism, but there are ways to restore it.
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
(Parameterized) Roles
1. (Parameterized)
Roles
Shawn M Moore
Best Practical Solutions
http://sartak.org
2. "The more I use roles, the less I
understand why anyone would
want to use the inheritance
model." - Ovid
"A role-aware type system allows
you to express yourself with
better genericity."
- chromatic
"Roles are the sound of diamond
inheritance that people have
stopped banging their heads
against." - hdp
( )
3. Example Role
package Counter;
use Moose::Role;
has counter => (
is => 'ro',
isa => 'Int',
default => 0,
);
sub increment {
my $self = shift;
$self->counter($self->counter + 1);
}
4. Example Role
package Counter;
use Moose::Role;
has counter => (
is => 'ro',
isa => 'Int',
default => 0,
);
sub increment {
my $self = shift;
$self->counter($self->counter + 1);
}
use Moose
5. Example Role
use MooseX::Declare;
role Counter {
has counter => (
is => 'ro',
isa => 'Int',
default => 0,
);
method increment {
$self->counter($self->counter + 1);
}
}
6. Consuming a Role
class Odometer with Counter {
method reset(Crook $you) {
$you->break_into($self);
$self->counter(0);
$you->plead('innocent');
}
}
with
8. Consuming a Role
class Odometer with Counter {
method reset(Crook $you) {
$you->break_into($self);
$self->counter(0);
$you->plead('innocent');
}
}
counter
9. Consuming a Role
class Odometer extends Widget with Counter {
method reset(Crook $you) {
$you->break_into($self);
$self->counter(0);
$you->plead('innocent');
}
}
10. class Odometer extends Widget {
has counter => (
is => 'ro',
isa => 'Int',
default => 0,
);
method increment {
$self->counter($self->counter + 1);
}
method reset(Crook $you) {
$you->break_into($self);
$self->counter(0);
$you->plead('innocent');
}
}
11. Class Building
Blocks
class Action::Throw
with Action::Role::Direction
with Action::Role::Item {
…
}
class Action::Melee
with Action::Role::Monster
with Action::Role::Direction {
…
}
12. Class Building
Blocks
class Action::Throw
with Action::Role::Direction
with Action::Role::Item {
…
}
class Action::Melee
with Action::Role::Monster
with Action::Role::Direction {
…
}
13. Class Building
Blocks
class Action::Throw
extends Action::Direction
extends Action::Item {
…
}
class Action::Melee
extends Action::Monster
extends Action::Direction {
…
}
20. Multiple Roles
Due to a method name conflict in roles
'Action::Direction' and 'Action::Monster', the
method 'name' must be implemented or
excluded by 'Action::Melee'
Moose
25. class Action::Melee
with Action::Direction
alias { name => 'direction_name' }
with Action::Monster
alias { name => 'monster_name' }
{
method name {
loc '%1 (at %2)',
$self->monster_name,
$self->direction_name;
}
}
2
26. Conflict Resolution
Due to a method name conflict in roles
'Action::Direction' and 'Action::Monster', the
method 'name' must be implemented or
excluded by 'Action::Melee'
27. Conflict Resolution
Due to a method name conflict in roles
'Action::Direction' and 'Action::Monster', the
method 'name' must be implemented or
excluded by 'Action::Melee'
28. Conflict Resolution
Due to a method name conflict in roles
'Action::Direction' and 'Action::Monster', the
method 'name' must be implemented or
excluded by 'Action::Melee'
78. See Also
Moose::Manual::Roles
"The Why of Perl Roles"
"Eliminating Inheritance via Smalltalk-Style
Traits"
"Traits - Composing Classes from Behavioral
Building Blocks"
Notas do Editor
Presented YAPC::Asia, 2009-09-11.
Tokyo Institute of Technology, Tokyo, Japan.
You've probably heard a lot about this new feature called "roles" lately. Not only are a lot of people talking about roles, but a lot of people are using roles. And for good reason!
Before I get into what a role is, and why roles are awesome, I want to just dive into what a role looks like.
You might notice a strong resemblance to regular Moose code. The only difference is that we "use Moose::Role" instead of "use Moose". This means that this package represents a role instead of a class. Oh, damnit, wait a second.
Much better. Anyway, so we have a role named Counter with an attribute and an increment method. Roles are not classes, so we can't call Counter->new. A role is more just a container for methods and attributes.
This is what consuming a role looks like. Here we have a class that needs a counter for keeping track of how many miles you have driven. "with" is the key word for consuming a role, much like "extends" is the key word for subclassing.
It is a very serious crime.
As you can see here, the role has added the counter attribute to Odometer. It also gained the increment method, which is presumably called by other parts of the system.
I must stress that consuming a role is not inheritance. You can still use inheritance alongside roles. What roles do are "flatten" its bits into the class.
The flattening is a lot like copying and pasting the code from the role into its consumer. That's even a pretty good description of what happens behind the scenes, though it doesn't happen at the strings-of-code level.
My favorite feature of roles is their reusability. One way to look at roles is that they are class building blocks.
If we factor out common behavior, we get to name that chunk of behavior, and reuse all that code. We know that these two actions have something to do with direction.
You might be wondering how any of this is better than multiple inheritance. Surely you know multiple inheritance is evil, right? RIGHT?
By the way, did anyone here even know how to use multiple inheritance with MooseX::Declare? I sure didn't until I wrote this slide.
Let's look at an example of why multiple inheritance is maligned. Here we have a class with a "name" method.
Now we have another class. It just has a direction.
And now we have a class that inherits from both of those.
So everything is fine right now. But at some point the requirements change.
We need a "name" method in Action::Direction for some other class.
This used to return monster name. Now this has changed without warning to the direction name, because Action::Direction is the leftmost parent. I hope your tests are very thorough!
This is a huge pain to debug. Code reuse and cleanliness is often not worth this pain, so we avoid multiple inheritance.
If these were roles instead, something very different happens.
Once we add the "name" method to Direction, we get this error. I'll talk about what it means soon, but one nice thing about this error is Moose throws it..
...immediately. You don't have to wait for the class to be instantiated, or the "name" method to be called. Moose throws conflict error at "with" time.
So this code is an error. But as alluded to in the error message, we can resolve this conflict. There are several ways to do this.
The class could define its own method. When a class defines a method and a role it consumes defines the same method, the class wins. So this resolves the conflict by overriding the "name" methods from the two roles. It's kind of like plugging your ears and yelling that everything is okay. But sometimes that really is all you need.
Another option is to exclude one of the conflicting methods. This way, Direction's "name" method is the one that is added to Melee. Obviously you can only use this where it makes sense. It probably doesn't make sense to use it in this example.
XXX: This syntax doesn't actually work yet as of 2009-08-22. A failing test has been submitted!
Another option is to combine the two methods. Here we disambiguate the two conflicting "name" methods, then use them in our own "name" method which serves both roles well enough.
I've found that this is usually the best solution.
So now we know what this error message means.
You can override the conflicting methods in the class, with or without reusing each conflicting method.
Or, if it makes sense, just exclude one of the methods so that it's no longer a conflict.
Try that with multiple inheritance. Yeah right!
Because of the flattening property of roles, and because of this conflict detection and resolution, the diamond inheritance problem doesn't apply to roles. Any ambiguity is a compile-time error, and the programmer has several tools to resolve such ambiguities. Role composition is much more pleasant to work with than multiple inheritance.
Suppose we have a role that assumes a particular method exists in each of its consumers. In this case, we're calling "tile" even though we don't know for sure that each consumer will have that method.
This code will work fine. We consume the role, Action::Chat gets the new methods, and everything is hunky-dory.
But at runtime when we try to call this method, we get an error.
This sucks. The author of the Monster role demands that consumers have a "tile" method. Perhaps the role's documentation does notify you that consumers must have it, but who even reads documentation any more? It would be nice to be able to codify this requirement.
And, of course, you can. If a role calls methods that it doesn't provide itself, the role should require them.
And like a conflict, we get this error at compile time. Much better than a method-missing error at runtime when we try to call ->tile. The Moose team really likes the fail-fast principle.
Here we have a role that requires an "entries" method, then builds on top of it with another method "ages".
We have an example backend that stores its entries in a hash table. We provide the "entries" method that the Scan role requires. That role gives us an "ages" method that calls "entries". Pretty straightforward use of roles.
When I was describing conflict resolution I mentioned how a method defined by the class wins over a method pulled in from a role. That's useful even outside of conflict resolution.
The ages method we define is doing a lot less work than the default implementation provided by the role would do. We don't need to pull in every field of every entry then pick out the values for age.
However, the role's implementation is a good default that would be useful for a lot of these backends. Method overrides permit reuse but allow optimizations or alternate implementations where needed.
You might be asking why we bother to consume Scan even though we don't actually pull in any methods or attributes from it. That's next!
Roles have so many excellent features that I am starting to agree with this (admittedly radical) viewpoint myself.
Allomorphism is a fancy word that means a few things. For one, roles can be part of the type system. It's a lot like duck typing, but more explicit.
Allomorphism also means that a role implies semantics. Basically, every method implemented or required by the role must implement some specified behavior.
These imply similar consequences, so I'm going to explain them together.
Here we have a role that requires "read" and "write" methods from each of its consumers. Given the name of the role, we can guess that the role requires these methods to be nonblocking.
Here we define a class that does the nonblocking interface. Nonblocking::Socket promises that its read and write methods fulfill the socket's nonblocking requirements.
Here's another class, one whose read and write methods do block. Even though it fulfills the method name requirements of Interface::Nonblocking, this class would be lying if it declared that it does the role.
We can ask an object if it does a particular role. This will die if we try to use a Blocking::Socket here.
This is better than checking "isa" because we actually care about capability. We don't care what $s's class is, or what its ancestors are. Any class can declare that it does the Interface::Nonblocking role, as long as it fulfills the role's contract.
Duck typing fulfills this same need. We also don't care about what class $s is, as long as it has the methods we want.
However, the problem with duck typing is that merely having a set of methods does not imply semantics. Perhaps read and write are actually going to do text-to-speech and printing a term paper. Or worse, they might block.
In any case, because the Interface::Nonblocking role requires the write method, we know that $s will not only have it, but we know it will not block.
Moose has support for allomorphism in attributes. Instead of demanding that connection be a particular class...
… we can demand that the value of connection does a particular role.
chromatic's point here is that you can stop caring about hierarchy and start caring about capabilities. Allomorphism means you don't need to subclass someone's crack-fueled module. You need only fulfill its crack-fueled interface. It's OOP freedom.
Here's a quick tip for you. Say you're extending a module. Ordinarily you'd write a subclass, right?
You write good tests, so you subclass its test subclass too.
You also have to subclass all the other subclasses you use. Maybe it'd be better to just monkeypatch WWW::Mechanize. Who'd know?
No! Don't do it! You'll screw it up for anyone else who happens to use that module in your codebase.
Just make your extension a role.
Now you can apply this role to the existing subclasses of the module. There's no dot-dot-dot in the braces here. This is a full class definition.
That's really it. You don't need any other code. All the extension code is in your role. Nice and clean.
There, I said it!
If you can implement your extension as a role, do it. Use inheritance sparingly.
I don't want to spend much time on this, but you can apply a role to a particular object. It doesn't have to be a full-blown class. It also won't affect any other objects. Which is good, because I wouldn't trust Jon with root.
Roles are nice for plugins too. I covered this heavily in my API Design talk. The idea is each plugin is just a role. These two modules make roles-as-plugins very easy.
http://sartak.org/talks/yapc-asia-2009/api-design/
To give you an idea of how to think about roles, here's a pretty simple metaphor that works for me. Classes are nouns.
Methods are verbs. They are simple behaviors. They do things.
Roles are adjectives. These are all good role names. If you think of some piece of behavior as an adjective, that's a good sign that it can be factored out as a role.
This is a very specific kind of parameterized role. Each consumer parameterizes the "ages" method by providing an "entries" method. But that's not really what I'm talking about when I say parameterized role. If you can do this, do it. If you need something more advanced...
Parameterized roles are my biggest contribution to Moose. I'm happy with how they came out.
Here we have our old Counter role, but now each consumer can declare what default it wants for the attribute. If they choose nothing, they get the default of 0.
rafl++ added parameterized role support to MooseX::Declare recently, so my examples get to look much nicer.
Using a parameterized role is pretty much the same. You just pass in the named arguments to "with". This was an example of parameterizing an attribute, and is probably the most common use of p-roles.
Here we have a role that wraps any method you give it with a bit of profiling code. This is sort of the inverse of "requires". Instead of the role telling you what method you need, you tell the role what method it needs to instrument. Someone once praised this type of p-role usage as resembling macros.
One really nice thing about parameterized roles is that they improve code reuse. All the consumer has to do is inform the role of which search engine they want to use. The role takes care of the rest. This means each consumer writes less code, because the parameterized role can build up more structure.
Here's another use case that was raised recently. The parameterized role could perform additional validation on each consumer. This is like "requires" for method names, but stronger.
This is what pre-MooseX::Declare parameterized roles look like, due to language constraints.
Roles are all of these things. I hope I've convinced you to use roles in your next project's design. Thank you!
Thanks to these people who have reviewed my slides and offered excellent advice.
Thank you to Ishigaki-san for translating my slides!