Class::Trait 0.10 now on the CPAN

Ovid on 2005-11-18T06:59:52

Thanks to advice from Joshua ben Jore (diotalevi), I have fixed the last known bug in Class::Trait. It's now on the CPAN and I just know that everyone's going to rush out and grab it because MI and mixins suck, right?

Well, no. Probably not. Even though the problems with multiple inheritance and mixins are well known, no one really seems to care that much. They like poking themselves in the eye with a stick because, hey, they don't poke themselves that often and they are used to how it feels.

The bug, by the way, involved trait methods getting flattened into the primary class even if the trait hadn't defined them. Anything which gets exported into the trait was going into your class and I needed to figure out the best way of knowing which methods the trait was really defining. That's when Joshua showed me the way out.

use B 'svref_2object';

sub _sub_package { 
    eval { svref_2object( shift )->STASH->NAME };
}

Pass that puppy a code ref and it will return the package the coderef was initially defined in. The following bit is how I use it:

my %implementation_for;

foreach  ( keys %{"${trait}::"} ) {
    my $method = "${trait}::$_";
    next unless defined &$method;

    # make sure we're not grabbing sub imported into the trait
    next unless _sub_package(\&$method) eq $trait;

    if ( /(DESTROY|AUTOLOAD)/ ) {
        die "traits are not allowed to implement $1\n";
    }
    $implementation_for{$_} = $method;
}
$trait_config->methods = \%implementation_for;


Class::Role

malte on 2005-11-18T11:42:54

Hi,

could you contrast Class::Trait to Class::Role?

Thanx!

Re:Class::Role

Ovid on 2005-11-18T17:58:24

Note that in the following that I do not use the word "role" even though that's the Perl 6 term. This is deliberate to make it clear I'm only talking about Perl 5 traits. Even when I discuss Class::Role I'll call it a trait. Yes, I know that's awkward :)

Well, first and foremost, Class::Trait is the only Perl module to have a substantially complete implementation of traits as described in the classic "traits paper" that introduced most programmers to traits.

As for Class::Role, it appears to have the same bug which Perl6::Roles also has and which Class::Trait just fixed.

Namely, it grabs all code in a package without checking to see whether or not that code actually should be flattened into the parent class (I'd write a test script to verify this but I couldn't get Class::Role to work on my box. Have you used it?) Class::Roles gets around that by requiring a role definition to list which methods are provided. I like that but I didn't want to change the Class::Trait interface.

Let's look at the sample trait at the top of the documentation for Class::Role:

package LongLiver;
use Class::Role;              # This is a role

sub profess {
    my ($self) = @_;
    print $self->name . " live a long time\n";
}

Where does the name() method come from? In the example code, that's provided in the class using the trait. That's a problem, though. If that method doesn't exist, it's a run-time error. Further, if you never call the profess() method in your code, you won't discover the bug. It would be awful if a critical production system uncovers the bug after your code goes "live".

Here's how that would be written with Class::Trait:

package LongLiver;
use Class::Trait 'base';
our @REQUIRES = qw(name);

sub profess {
    my ($self) = @_;
    print $self->name . " live a long time\n";
}

Now, if name() does not exist in the calling class (either directly or from some other trait being used), your code won't even compile.

requirement (name) for LongLiver not in Donkey

I believe that Class::Trait is the only Perl trait implementation which implements this. Further, if you read the traits paper, I believe it's critical that this feature be present. Here are the major features that Class::Trait is designed to support:

  • Only export methods defined in the trait
  • Conflict resolution via method renaming
  • Conflict resolution via denying a method (so another trait can provide it)
  • Requirements must be explicitly met or we fail at compile time
  • Respect the above behaviors even when aggregating traits into a new trait

I do have more plans for Class::Trait but for right now it's fairly feature complete. My major concern is to make "deferred" methods available. That is, a class or trait should be able to "promise" a method and, even if it doesn't exist at compile time, it's "promised" to exist when called. This should make Class::Trait more Perlish.

Runtime role (trait) composition

Tim Bunce on 2005-11-19T12:44:22

The Perl6::Roles module supports runtime role composition:

      Name::Of::Role->apply( $some_object_of_unknown_class );

Can (does/will) Class::Traits support that?

Re:Runtime role (trait) composition

Ovid on 2005-11-19T16:32:17

It's possible that it will. For the time being, I'm doing some deep internals work with it to make sure that I have everything nailed down. After I feel truly comfortable with its current state I'll start expanding it.

Re:Runtime role (trait) composition

Tim Bunce on 2005-11-19T22:44:42

I need runtime role composition for DBI v2 prototyping which will be layered over my JDBC module. JDBC can return handles to funky internal Java classes that vary with the driver being used.

Re:Runtime role (trait) composition

Ovid on 2005-11-19T23:58:47

Currently there is a workaround:

use Class::Trait;
Class::Trait->import('TTrait1', 'TTrait2');
Class::Trait->initialize;

That will properly use those traits, report conflicts, etc. However, it only works once. If both of those traits have identically named methods, the initialize() will fail. The following will not work:

use Class::Trait 'TTrait1';
Class::Trait->import('TTrait2');
Class::Trait->initialize;

This is because by the time the second trait is used, the meta information for the first is not available. Any methods in Trait2 which were already in TTrait1 will be silently ignored. There is, however, a reflection class avaiable. I'll see if I can use that so that eventually you can do this:

use Class::Trait 'TTrait1';

# later

Class::Trait->apply('TTrait2');

Re:Runtime role (trait) composition

Tim Bunce on 2005-11-21T11:12:33

While that would be runtime it's not 'dynamic' (for want of a better word). I looks like I'd have to do something like this:

        eval sprintf "package %s; Class::Trait->apply(%s);",
                ref $some_object_of_unknown_class, $trait_class;

whereas I'd like to be able to do:

            $trait_class->apply( $some_object_of_unknown_class );

(which would rebless $some_object_of_unknown_class into a new class) Both $trait_class and $some_object_of_unknown_class are unrelated to the current package.

Basically I'm after the run-time behaviour described by
http://dev.perl.org/perl6/doc/design/syn/S12.html#roles
---quote---
Run-time mixins are done with does and but. The does binary operator is a mutator that derives a new anonymous class (if necessary) and binds the object to it:

        $fido does Sentry
The does operator returns the object so you can nest mixins:

        $fido does Sentry does Tricks does TailChasing does Scratch;
Unlike the compile-time role composition, each of these layers on a new mixin with a new level of inheritance, creating a new anonymous class for dear old Fido, so that a .chase method from TailChasing hides a .chase method from Sentry.
---quote---

Re:Runtime role (trait) composition

Tim Bunce on 2005-11-21T11:16:07

[Darn. I hit submit instead of preview.]

I just wanted to add a thank you for working on this.

Re:Runtime role (trait) composition

Ovid on 2005-11-21T18:06:45

whereas I'd like to be able to do:
$trait_class->apply( $some_object_of_unknown_class );
(which would rebless $some_object_of_unknown_class into a new class) Both $trait_class and $some_object_of_unknown_class are unrelated to the current package.

This is where I'm not sure I'm following you. It sounds like what you want is prototyped OO whereby one can add new methods to an instance and not just to the class itself, is that correct? However, I'm not sure that your syntax is good. How would that chain? If I start from the instance, it seems clear. So assuming that $fido is a Dog who inherits from Mammal:

$fido->apply('Sentry')
     ->apply('Tricks')
     ->apply('TailChasing')
     ->apply('Scratch');

(Of course, those could all be done in separate statements for those who don't like chaining).

And then that would actually get represented as this?

+--------+
| Mammal |
+--------+
    |
    V
+--------+
|  Dog   |
+--------+
    |
    V
+--------+
|  Anon  | (Role:  Sentry)
+--------+
    |
    V
+--------+
|  Anon  | (Role:  Tricks)
+--------+
    |
    V
+--------+
|  Anon  | (Role: TailChasing)
+--------+
    |
    V
+--------+
|  Anon  | (Role:  Scratch)
+--------+

If that's correct, I can make this work. My big concern, though, is having a programmer not realize that TailChasing has a chase() and thereby silently and unknowingly overriding the chase() method in Sentry or Tricks. One could argue that we have this problem when we write a subclass, but we're far more likely to know what the superclass is providing. There's no messing with inheritance at runtime involved. Of course, if this is just prototyped OO, then it makes sense why this is useful, but I'm still concerned, though now that I stop to think about this, just overriding the methods this way is easier than what I've been doing :)

Or maybe I'm just paranoid and should trust Larry and the programmers who will use this?

Oh, and you're quite welcome (re: your later thanks). We've been happily using traits at Kineticode and have been pleased with the problems they have solved.

And FYI: it's easy to add code on use.perl. Just use the <ecode>$code</ecode> tags.

Re:Runtime role (trait) composition

Ovid on 2005-11-21T18:40:18

D'oh! Even better:

+--------+
| Mammal |
+--------+
    |
    V
+--------+
|  Dog   |
+--------+
    |
    V
+--------+
|  Anon  | (Roles:  Sentry, Tricks, TailChasing, Scratch)
+--------+

With that, a class with a runtime trait applied will have subsequent runtime traits overriding methods in the same anonymous class. The heirarchy is flatter, we use a LIFO strategy for trait method flattening, performance is better and (I think) behavior is the same.

Re:Runtime role (trait) composition

Tim Bunce on 2005-11-21T23:23:12

This is where I'm not sure I'm following you. It sounds like what you want is prototyped OO whereby one can add new methods to an instance and not just to the class itself, is that correct?

Yes. Or more specifically, I want to get as close to S12 as possible. I'm hoping you can follow the Perl6 approach to roles (http://dev.perl.org/perl6/doc/design/syn/S12.html#roles) as closely as practical.

S12 says compile-time composition merges into one class and detects collisions (as Class::Trait does now), but run-time composition layers on extra anon classes. Very few people will want runtime composition (just me so far!) and I'd guess that most of those who do would be keen to have Perl6 semantics. (The performance difference wouldn't be significant as method lookups are cached.)

Also, I'd hope that given

    $fido = new Dog;
    $fido->apply('Sentry')->apply('Tricks')->apply('TailChasing')

    $biffo = new Dog;
    $biffo->apply('Sentry')->apply('Tricks');
then $biffo could reuse part of the class tree created for $fido. But I guess that's just an optimization.

Re:Runtime role (trait) composition

Ovid on 2005-11-21T23:58:12

I don't see $biffo reusing part of the class tree for a few reasons. First, I am hoping to not layer on more than on anonymous class, though I might. Instead, I was hoping to add one anonymous class layer and flatten new traits in that class. That makes things really, really simple. However, there's a catch. If I can apply a trait, I should be able to remove it, too. That means that having a chain of anonymous classes may be the way to go (as removing a trait would effectively be like removing an item from a linked list). This affects your example because runtime traits are specific to a given instance and removing a trait from one instance should not affect others.

And at Kineticode, we have realised that we also need runtime trait application, so we're in the same boat. In fact, some of the future development of Class::Trait is likely to be sponsored directly by Kineticode.

I will read the Perl 6 approach more carefully. Right now, I want to keep my current compile time semantics lest I break anyone's code currently using it. Runtime semantics are fair game though.

Re:Runtime role (trait) composition

Tim Bunce on 2005-11-22T14:31:20

I've no need to remove traits and, as far as I recall, it's not mentioned in S12. Seems like it would greatly complicate the implementation for little gain.

S12 does say You can also mixin a precomposed set of roles:

        $fido does Sentry | Tricks | TailChasing | Scratch;

This will level the playing field for collisions among the new set of roles, and guarantees the creation of no more than one more anonymous class.


That sounds like it matches what you're proposing (a single anon class with collisions detected) and could be requested using

    $fido->apply(qw(Sentry Tricks TailChasing Scratch));

whereas it seems natural (to me) that each apply() call should create a new anon class (if there isn't a matching one already).

Re:Class::Role

malte on 2005-11-19T12:45:16

We are using Class::Role in a very large production system. There were never any problems in installing it.

You make some very good points about traits there. I was probably thinking too much in terms of "smart Java interfaces" to see the whole power of traits. The reason we originally chose to use Class::Role was the name. I really like the term role, because it creates nice pictures in my head :).

Basically the only thing we are using roles for at the moment is to implement methods which are empty in the super class of the class that uses the role while only using methods within the role that are guaranteed to exist in the super class. (Does the sentence make sense?). The entities within our OR-Mapper for example can assume certain roles:

package Thing;
use base "Entity";
use Class::Role "Historian";

An entity that is also a historian will save all changes made to itself within a special history table.

Thank you for your answer. We will probably switch to Class::Trait for future projects.

Re:Class::Role

Ovid on 2005-11-19T18:42:44

Are you saying, in your example above, that "Entity" is effectively a Java interface and "Historian" provides the implementations? If so, that's a very interesting way of going about things. I'll have to think about that.

Re:Class::Role

malte on 2005-11-19T20:15:38

That would indeed be an interesting way to go about it :) However, in that case Entity is an abstract class, that provides for database persistence and data relationship management (or-mapping). The abstract class provides for hooks that are filled in by the role.

Re:Class::Role

malte on 2005-11-19T12:58:38

By the way: The same deficiency could be pointed out about the base pragma. If it would call import on the super class, an abstract super class could enforce its child the implement certain methods.

I don't know if Class::Trait implements this, but it should be possible for a class that uses a trait to declare that it it is also a trait or abstract class that requires certain methods to be implemented by child classes. If both traits require a "name" method to be implemented, that should not be a compile time error.

Re:Class::Role

Ovid on 2005-11-20T00:09:20

it should be possible for a class that uses a trait to declare that it it is also a trait or abstract class that requires certain methods to be implemented by child classes.

If I understood you correctly, it sounds like you're asking for traits to be composed of other traits and for all of those traits to specify required methods even if the composite of all of those traits does not provide them, thus requiring the class using the traits to make them available. If that is what you mean, then yes, Class::Trait supports this. I've recently uploaded version 0.11. If you check the new t/99_trait_errors.t tests, you'll see code like the following:

#
# Extra::TSpouse: explode [ requires lawyer() ]
#           does: Original::TSpouse: fuse explode [ requires alimony() ]
#

clean_inc();
eval <<'END_PACKAGE';
package Polygamy;
use Class::Trait 'Extra::TSpouse';
Class::Trait->initialize;
sub lawyer {}
END_PACKAGE

ok $@,   'Trying to load a trait which does not meet requirements should fail';
like $@, qr/\QRequirement (alimony) for Extra::TSpouse not in Polygamy\E/,
  '... and @REQUIREMENTS should bubble up correctly';

In this code, the trait Extra::TSpouse uses the trait Original::TSpouse. The extra spouse requires a lawyer (note that this method is supplied) and the original spouse requires alimony. Because of the way traits are flattened into classes, the alimony trait is shown for the extra spouse, but the problem is easy to track down.

I trust you'll forgive my politically incorrect tests :)

Re:Class::Role

sigzero on 2005-11-20T20:36:33

That is an interesting choice of topics for a test module.

Re:Class::Role

sigzero on 2005-11-20T20:37:04

Oh...I forget the smiley. : )