> From: Jesse Luehrs <d...@tozt.net>

>No, overriding is a perfectly valid way to resolve conflicts, and always
>has been. alias and excludes are only important to fix cases where this
>isn't possible, and this change should remove the last of those.


So I can avoid spamming the list regarding things already hashed out, can you 
point me to the prior discussions where this change was decided? I'm hoping to 
better understand other's thinking on this (I've discussed this with mst, but I 
don't think it would be fair to assume that his opinions apply to everyone 
else).

My concern about removing "alias" and "excludes" stems in part from how I 
appear to have a slightly different philosophy about OO programming. I view 
objects as experts about solving a given problem, but the underlying object is 
composed of "legos" of roles. Need some new behavior? I rummage around in my 
toy box looking for the legos to snap onto my existing object. Viewing each 
object as a collection of well-defined behaviors rather than "how can I squeeze 
this into a tree or graph hierarchy" lets me just get things done, though I 
realize I may be in the minority here.

For example, in my first programming job, I worked several hours a week as a 
programmer and the rest of the week I was a glorified clerk. I was (illegally) 
paid at different rates for each job. So the "employee" sometimes behaves as a 
clerk and sometimes behaves as a programmer. Calculating my pay with roles is 
fairly straightforward (funky formatting to make it easier to fit in the email):

    { package R::Programmer; use Moose::Role; sub pay_rate {12} }               
                                                               
    { package R::Clerk;      use Moose::Role; sub pay_rate {10} }
    { package R::Employee;   use Moose;
        with 'R::Programmer' => {
            # this is why I want a "rename" property here
            exclude => 'pay_rate',
            alias   => { pay_rate => 'programmer_pay_rate' }
          },
          'R::Clerk' => {
            exclude => 'pay_rate',
            alias   => { pay_rate => 'clerk_pay_rate' }
          };
        has [qw/hours programming_hours drudgery/] => ( is => 'ro' );
        sub pay_rate {8} # ugly hack for quick example. Oops

        sub paycheck {
            my $self = shift;
            return $self->programmer_pay_rate * $self->programming_hours
                 + $self->clerk_pay_rate      * $self->drudgery;                
                                                               
        }
    }
    my $employee = R::Employee->new(
        programming_hours => 15,
        drudgery          => 25,
    );
    say $employee->paycheck;

(yes, there's some cruft in this quickly hacked together example and it's 
deliberately kept simple (e.g., pay_rate) to focus on the role composition 
issue)

How do I do that without roles? I'm not going to fall back on MI because that 
introduces the very problems that roles were trying to solve! Thus, delegation 
seems to be the trick (if I'm creating a straw man argument here, my apologies. 
Please correct me). Here's the first pass at that:

    {   package R::Paycheck; use Moose::Role;                                   
                                                               
        requires 'pay_rate';
        has [qw/hours_worked/] => ( is => 'ro' );
        sub paycheck {
            my $self = shift;
            return $self->hours_worked * $self->pay_rate;
        }
    }
    { package C::Programmer; use Moose; with 'R::Paycheck'; sub pay_rate { 12 } 
}
    { package C::Clerk;      use Moose; with 'R::Paycheck'; sub pay_rate { 10 } 
}
    { package C::Employee; use Moose;
        has [qw/programming_hours drudgery/] => ( is => 'ro' );
        has 'programmer' => ( is => 'ro', lazy => 1, builder => 
'_build_programmer' );
        has 'clerk'      => ( is => 'ro', lazy => 1, builder => '_build_clerk' 
);

        sub _build_programmer {
            my $self = shift;
            C::Programmer->new( hours_worked => $self->programming_hours );
        }

        sub _build_clerk {
            my $self = shift;
            C::Clerk->new( hours_worked => $self->drudgery );
        }

        sub paycheck {
            my $self = shift;
            return $self->programmer->paycheck + $self->clerk->paycheck;
        }
    }

In some respects it's cleaner, but in other respects, it's not. For example, 
many of my roles are things that shouldn't necessarily be instantiated on their 
own (Serializable), but if I'm forced to use delegation for composition because 
I don't have exclusion and aliasing, then sometimes I'm going to have to fall 
back on the old hack of creating classes to add some behavior, even if the 
"class" shouldn't really be a class. (See also: 
http://steve-yegge.blogspot.fr/2006/03/execution-in-kingdom-of-nouns.html).
Further, while I realize my "lego" approach to building classes isn't to 
everyone's tastes, it does tend to shake out bugs in role implementations very 
quickly. For example, I've had to give up on Moo because its roles are so buggy 
(https://rt.cpan.org/Public/Bug/Display.html?id=82711). So unless there's a 
really concrete, demonstrable problem with aliasing and excluding, I'm very 
concerned about taking away these tools that have been in my toolbox for many 
years.

Finally, we can consider performance. When a role's methods are flattened into 
my class, they're going to be faster than delegation. 


    use Benchmark;                                                              
                                                               
    my %args = (
        programming_hours => 15,
        drudgery          => 25,
    );
    timethese(
        100000,
        {   roles   => sub { R::Employee->new(%args)->paycheck },
            classes => sub { C::Employee->new(%args)->paycheck },
        }
    );

In this example, we see that roles are more than twice as fast as delegation:

    Benchmark: timing 100000 iterations of classes, roles...
       classes: 14 wallclock secs (14.20 usr +  0.00 sys = 14.20 CPU) @ 
7042.25/s (n=100000)
         roles:  6 wallclock secs ( 5.63 usr +  0.00 sys =  5.63 CPU) @ 
17761.99/s (n=100000)

As it turns out, object creation is very expensive, so if we skip that part and 
just call the salary method (and we'll bump this up to one million iterations):

    use Benchmark;
    my %args = (
        programming_hours => 15,
        drudgery          => 25,
    );
    my $roles   = R::Employee->new(%args);
    my $classes = C::Employee->new(%args);
    timethese(
        1000000,                                                                
                                                               
        {   roles   => sub { $roles->paycheck },
            classes => sub { $classes->paycheck },
        }
    );

    Benchmark: timing 1000000 iterations of classes, roles...
       classes:  3 wallclock secs ( 2.88 usr +  0.00 sys =  2.88 CPU) @ 
347222.22/s (n=1000000)
         roles:  0 wallclock secs ( 1.28 usr +  0.00 sys =  1.28 CPU) @ 
781250.00/s (n=1000000)

We still see the roles as slightly twice as fast as delegation.

In short:

1. Allowing a role to silently override a consumed role's methods moves Perl 
roles even further away from Smalltalk-style traits and its associative and 
commutative guarantees.

Regarding plans to remove 'alias' and 'excludes':

2. Intending to remove the aliasing and excluding composition tools removes 
tools that allow finer control over how you consume your objects.
3. Removing certain use cases for role composition and forcing developers to 
fall back on delegation is a significant performance hit.

Cheers,
Ovid--
Twitter - http://twitter.com/OvidPerl/
Buy my book - http://bit.ly/beginning_perl
Buy my other book - http://www.oreilly.com/catalog/perlhks/
Live and work overseas - http://www.overseas-exile.com/

Reply via email to