If you've followed Ovid's use Perl journal recently (and you should), you've seen a lot of practical discussion over the use of Perl roles in his work at the Beeb. There's also plenty of theoretical discussion.
I joined the Perl 6 design team in 2003. I spent a lot of time thinking about problems in every object system I'd ever seen. When we released Apocalypse 12 in 2004, we included a new feature called roles. The design borrowed from a Smalltalk idea called Traits, but (as usual) we Perl people have our own take on things.
Rakudo Perl 6 provides a very usable implementation of roles, and the Moose object system for Perl 5 allows you to use them as well.
Roles are different from so-called traditional class-based or prototype-based or even multidispatch object systems in several important ways.
Role Goals
In my mind, object orientation provides two essential features: encapsulation and polymorphism. A well-designed object oriented system models domain concepts along well-defined boundaries, hiding internal details from the outside world and treating like concepts as like concepts because they share the same interfaces.
Careful readers will note that the word "inheritance" does not appear in that paragraph. That is no accident.
Polymorphism is an interesting concept. I see it as providing genericity and extensibility. I may not know all of the potential uses for an object when I create it, but I need to have an accurate idea of how the object behaves. I need to be able to name that collection of behaviors. If I've created the object well, the collection of behaviors the object provides has a meaningful and understandable name.
A role is a name for a discrete collection of behaviors.
When you want to perform operations on an object, you need to know what kinds of behaviors that object supports. Is it a GUI widget? Does it represent a customer? Can you introspect it? Can you serialize it? Does it know how to log debugging information?
In effect, you're asking the object "What do you do? Do you perform this role?"
Once you start asking that question, you can (and should) stop caring about how the object performs its role. I've said nothing about inheritance, or delegation, or composition, or allomorphism. Those are mechanisms. Those mechanisms should be well-encapsulated inside the object where you can't poke or prod at them because they're none of your business.
The important question is not "Do you have a method called log_debug_output
?" or "Do you inherit from DebugLogger
?" but "Do you perform the DebugLogging
role?"
That's subtle, so let me repeat it a different way. If you write code
mindful of roles and you don't know the specific class of an object you receive
but you want to call a method called log_debug_output
on that
object and have it behave as you expect, you want to check that that object
performs the DebugLogging
role. It doesn't matter how
the object has that method. It could inherit it from a superclass. It could
mix it in from a collection of unbound accessory methods. It could delegate it
to a contained object or proxy it to a remote object. It could reimplement the
method directly. It could compose it in from a role. It doesn't matter
how the object has that method -- that's none of your business outside
of the object -- only that it does have the method, and that it understands
that method in terms of the DebugLogging
role.
That last part is also subtle. The duck typing hypothesis suggests that
method names alone are suitable to determine appropriate behavior. Roles
avoids problems there by requiring disambiguation through naming. A
TreeLike
role's bark
method has an obviously
different context from a DogLike
role's bark
method.
Roles allow you to express (or require) context-specific semantics, especially when combined with your type system. A role-aware type system allows you to express yourself with better genericity: as long as you hew to a well-defined interface and do not violate the encapsulation of objects, you can enforce well-typed programs based on the specific behavior of objects regardless of their implementation.
This is very theoretical. Don't worry; I'll show specific examples in future entries.
Role Features
Roles are more than just tagged collections of behavior. You can think of them as partial, uninstantiable classes.
Roles can provide default implementations of behavior they require. By composing a role into a class, you can import its methods (and other attributes) into the class directly at compile time. There are rules for disambiguation and conflict resolution and requirements and provisions, but you get the rough idea. A role also provides a mechanism for code reuse.
Parametric roles take the concept even further by customizing their composable behavior based on arguments provided at composition time. They're intensely powerful.
The final -- and perhaps most subtle -- feature of roles comes from building
them into your type system. Every class implies the existence of a role. If
you declare a DebugLogging
class, other code can access the
DebugLogging
role. They may not be able to compose in behavior
from that class -- unless you write the class to make that possible -- but they
can say that they perform the DebugLogging
role, with all of the
concomitant role composition checks to enforce this, to produce an object which
may successfully substitute for a DebugLogging
object anywhere
that expects a DebugLogging
object -- even though there's no
formal relationship between the original class and the class which performs its
role.
As I said, this is powerful but theoretical. Tomorrow I'll discuss Perl roles versus inheritance.