-
Notifications
You must be signed in to change notification settings - Fork 19
Proposal for Multiple Role Applications
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.
One of the many benefits of roles is that they can provide shared behavior across classes which isn't necessarily exclusive to that class. For example, Role::REST::Client provides REST (REpresentational State Transfer) capabilities to your classes, even if those classes aren't related by inheritance. These types of behaviors are often referred to as cross-cutting concerns.
But roles also provide something different. Years ago I was writing about "synthetic" versus "natural" code, using terminology borrowed from Mark Jason Dominus. He's since started referring to that as "structural" versus "functional" code and I'll use that terminology here.
"Functional code" is code which solves the business problem you have. It's the code we usually love to write.
"Structural code" is code you write to work around technical limitations of your programming language. In fact, there's a well-known book called "Design Patterns" which is all about when and how to write that structural code correctly.
In Cor, we make the following assumption:
We want to spend more time writing functional code which solves our problems and less time writing structural code which solves our programming language's problems.
If your programming language is well-designed, you can often skip many design patterns.
For example, in Perl, we rarely use linked lists. Our language is structured
in such a way that efficient insertion and deletion of list elements is provided
via splice
.
However, we still have structural issues. For example:
sub control_break {
my $self = $_[0];
if ($_[1]) {
$self->{control_break} = $_[1];
}
return $self->{control_break};
}
The code above tries to provide both a reader and a writer for the control_break()
slot, has a common bug associated with this type of structural code (can't set it to a false value), and there are at least 70 or
80 modules on the CPAN which have tried to write structural code to solve the general
case of the above code. Cor, of course, is trying to make that problem go away.
Roles, however, have some interesting possibilities of offering code reuse in a way we haven't explored before: minimizing delegation.
I love delegation. If I have another class performing some behavior I need my current class to access, delegate! I use this often enough that I've written Method::Delegation to handle the common cases for me in classes where Moo/se isn't being used. Again, structural code, but very useful.
However, sometimes when talking about roles, people say "I'd just use delegation for that." If there's an existing class which provides needed behavior, that's fine. Delegation is often a great choice. It can be an awkward choice if the delegate needs to exchange a lot of information with the consuming class (potentially tight coupling). But if I can simply compose a role into my current class and get the desired behavior, what do I delegate to? Nothing. But I'm sometimes told I should create a brand-new class for no other reason than to have a delegation target. See also: "structural code."
Delegate when you already have a reasonable delegation target, but don't just say "delegate" for the sake of delegation.
Roles, in this respect, minimize our delegation because we often don't need a delegation target. But what if a role provides useful behavior a class wishes to consume more than once? For example, if a role offers a cache and you need two separate caches in your class, what do you do? Well, you might find yourself simply creating a new class to consume that role and then you instantiate that new class twice.
But do we really need to do that? If that class has no other purpose than to provide some behavior to your existing class, isn't this clearly the type of structural code that we would like to have go away?
As it turns out, we can make this go away if we allow a class to consume a role more than once.
In the Grace programming language, you can re-apply roles (called "traits" in Grace). Here’s an example:
method counterTrait {
// this method is a trait — it returns a fresh object that has two methods, but no fields
// That object does have state, though — the captured value of counter.
var counter:Number := 0
object {
method inc { counter := counter + 1 }
method value { counter }
}
}
class twoCounters {
// this class uses the counterTrait twice — but aliases and excludes
// both methods to avoid a trait conflict.
use counterTrait
alias inc1 = inc
alias value1 = value
exclude inc
exclude value
use counterTrait
alias inc2 = inc
alias value2 = value
exclude inc
exclude value
method incFirst { inc1 }
method incSecond { inc2 }
method showBoth { "counter1 = {value1}; counter2 = {value2}" }
}
// try it out
def c = twoCounters
c.incFirst
c.incFirst
c.incSecond
print(c.showBoth)
The output is counter1 = 2; counter2 = 1
, as you would expect. Each trait
has its own hidden state.
In Cor, we could do the same thing:
role CounterRole {
has $counter :reader = 0;
method inc () { $counter += 1 }
}
class TwoCounters does CounterRole <rename: inc => inc1,
rename: counter => value1>,
does CounterRole <rename: inc => inc2,
rename: counter => value2>
{
method inc_first () { $self->inc1 }
method inc_second () { $self->inc2 }
method to_string () {
sprintf "counter1 = %i; counter2 = %i", $self->value1, $self->value2;
}
}
my $c = TwoCounters->new;
$c->inc_first;
$c->inc_first;
$c->inc_second;
print $c;
Each subsequent application of a role would introduce a new lexical scope over the slots bound to that scope, keeping them separate.
This is interesting because we have greater reusability. Do you have a role which provides a cache and your class needs two caches? To reuse the role, you’d have to create “stub” class just to consume the second instance of that role and delegate. This proliferation of classes just to work around language limitations defeats the purpose of roles.
Multi-role application reduces structural code and let's us focus on functional code.
However, the semantics are new and not everyone on the #cor IRC channel was convinced. I would love to hear more feedback on pros and cons.
One con is a huge semantic difference. Currently with roles, if a class excludes or aliases
methods from a role, the class is required to implement methods with the original names. The
idea is that $object->does('Some::Role')
would provide a guarantee that those methods
exist. In theory, a class is "class + state + roles", but in reality, the roles composed
into the class are defined via the name of the role and the methods actually provided, not
just the methods the role originally provided. I am not aware that we have MOP support for
this distinction, nor am I aware that people actually take advantage of this behavior.
With multi-role application, the class above would no longer necessarily be
required to implement the role’s inc
and counter
methods. I’m unsure if
that’s really an issue, but it’s worth keeping in mind.
Corinna—Bringing Modern OO to Perl