Skip to content
Ovid edited this page Sep 14, 2021 · 13 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.

History

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.

Writing a Role

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.

Behaviors

  1. When consumed, a role flattens all methods (not subs) into the consumer
  2. Slots are not flattened into the consumer
  3. Methods created by slot attributes (e.g., :reader) are flattened into the consumer
  4. Composing multiple roles is commutative ((A + B) = (B + A))
  5. Composing multiple roles is associative ((A + B) + C = A + (B + C))
  6. Barring aliasing or exclusion, any methods with duplicate names will generate an exception
  7. 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.

Method Modifiers

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 and Excluding

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.

Excluding Methods

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

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()          { ... }
    ...
}
Clone this wiki locally