Skip to content

Proposal for Multiple Role Applications

Ovid edited this page Sep 14, 2021 · 16 revisions

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.

Proposal: Multiple Role Application

Background

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.

Multi-Role Application in Grace

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.

Multi-Role Application in Cor

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.

Clone this wiki locally