-
Notifications
You must be signed in to change notification settings - Fork 19
Roles
Please see the main page of the repo for the actual RFC. As it states there:
Anything in the Wiki should be considered "rough drafts."
Click here to provide feedback.
Roles, originally called "traits", worked their way into Perl via Smalltalk. In particular, the paper Traits: Composable Units of Behavior was widely circulate and cited. It's very easy to read and explains the problem very clearly, though it made some silent assumptions about the tooling that were not clearly explained (this is from my direct correspondence with the authors).
So I strongly recommend a follow-up paper of theirs, Traits: The Formal Model. I'll refer the specifics of roles to this paper (and the "Composable" paper) and just list a summary here.
While you should consult the grammar, writing a role looks like this:
role DoesSomething v0.1 does Something::Else, Some::Other::Thing {
has ...
has ...
method foo ...
method bar ...
}
And in your class:
class MyClass does DoesSomething {
}
And now MyClass
will have the foo
and bar
methods flattened into it, just as if they were written into the class.
- When consumed, a role flattens all methods (not subs) into the consumer
- Slots are not flattened into the consumer
- Methods created by slot attributes (e.g.,
:reader
) are flattened into the consumer - Composing multiple roles is commutative (
(A + B) = (B + A)
) - Composing multiple roles is associative (
(A + B) + C = A + (B + C)
) - Barring aliasing or exclusion, any methods with duplicate names will generate an exception
- If you class provides a method with the same name as one of the role methods, an exception will be generated unless you alias or exclude the role method
The commutative and associative properties are extremely important and sometimes overlooked. They help to avoid one of the key issues with inheritance; specifically, that order matters. Rather than get distracted by this, I'll write up a separate page if it's controversial.
The last point, however, is a bit controversial. The intent is to ensure that "big systems" are safe to manage. For example, if you're working on a million line codebase, it's easy enough for a new developer to add a auth
method to a class, not realizing they just silently overrode the Role::Security
role's auth
method. And hey, you have 32% code coverage, so your tests don't catch it either and it gets rolled (role(d)?) into production and everything blows up.
Or you can program in Cor, silently override that auth
method and the code won't even compile (or maybe it will just warn).
Note: The reason for this difference is that, in the "Composable" paper, there was clear, explicit discussion of classes overriding role methods automatically. However, the authors were working with a specially modified Smalltalk browser that highlighted if a class method was overriding a role method. Thus, there was no silent change of behavior. In correspondence with the authors, they stated that if your language doesn't provide the tooling support to alert the developer of this case, they felt roles would be unusable if they silently allowed their behavior to be overridden.
It is not yet clear if Corinna will provide method modifiers. However, if it does, Stevan and I are in agreement that they won't be allowed for role methods. This is because they break commutative and associative contracts.
For example, if a method returns a number and has two modifiers applied to it. One modifier adds a percentage and another adds a fixed amount. The final result depends on the order in which those modifiers get applied.
Aliasing (renaming) role methods and excluding role methods are important features. Generally speaking, you should construct your roles in such as way as to avoid this. However, sometimes it needs to be done (for example, when using a role that's shared by other teams or pulled from the CPAN and you can't control the interface).
For the purpose of the following, we'll assume the following role:
role DoesJSON {
# fake JSON module so we can avoid arguments about
# which JSON module we should be using here
use Some::JSON::Module qw(
encode_json
decode_json
);
requires to_hash;
method to_json() {
return encode_json($self->to_hash);
}
method from_json($string) {
# relies on decode_json throwing its own exception
return decode_json($string);
}
}
The above role requires that, when consumed, the consuming class has a
to_hash
method. The class might implement that method, inherit that method,
delegate to that method, or have it provided by another role. So long as the
$class->can('to_hash')
at role application time, the requirement is
satisfied.
The role provides two methods, to_json()
and from_json($string)
. The
encode_json
and decode_json
subroutines are not provided because they are
not methods.
If DoesJSON
were to consume another role, the other role's methods would be
combined with DoesJSON
and those methods flattened into the final consuming
class.
We have not yet updated the Corinna Grammar with the following as the following is a WIP.
When consuming a role, immediately after the role name, a comma separated
list of exclusions and aliases may be supplied, wrapped in square brackes.
For exclusion, we prefix the method name with a -
(minus) symbol.:
class Customer does DoesJSON [-from_json] {
method to_hash() { ... }
method from_json($string) { ... }
...
}
Whitespace is allowed, so [ - from_json ]
is fine.
All methods provided by a role are automatically required
by that role, so
if we exclude the from_json
method, the class will still need to provide
that method. This is because roles are black boxes and just because we chose
to exclude the method doesn't mean that the class doesn't use it internally.
As another reason why it's required is the following:
if ( $object->does('DoesJSON') ) {
my $json = $object->from_json($string);
}
The fact that an object does
a given role is implicitly a contract that code
should be able to rely on.
If the Customer
class has both from_json
and to_json
methods, it still
might choose to consume the DoesJSON
role to programmatically signal to other classes that
this behavior is supported. So both methods must be excluded:
class Customer does DoesJSON [ -to_json, -from_json ] {
method to_hash() { ... }
method from_json($string) { ... }
method to_json() { ... }
...
}
The above effectively turns DoesJSON
into an interface.
Aliasing methods works like excluding methods, but instead we're renaming
them. Simply separate the old method name from the new method name by a fat comma
(=>
). Whitespace is allowed. Note that because the role offers a contract,
the old method name is still required. Thus, this really isn't a tool to get
more "aesthetically pleasing" method names. It's for when you have a method
with the same name, but you still need the original functionality.
class Customer does DoesJSON [ from_json => _from_json_old ] {
method to_hash() { ... }
method from_json($string) { ... }
...
}
You can, of course, combine exclusion and aliasing.
class Customer does DoesJSON [ -to_json,
from_json => _from_json_old ] {
method to_hash() { ... }
method from_json($string) { ... }
method to_json() { ... }
...
}
Corinna—Bringing Modern OO to Perl