A simple explanation of traits

Ovid on 2005-11-19T19:31:57

Someone on the Portland Perl Mongers list asked for a simple explanation of traits. Here it is.

What I love about perl's oo is that we really don't need all of the formalisms that appear in other languages.

Yes and no. There's no question that Perl's model is extremely flexible, but sometimes you want to break instead of bend. The problems with multiple inheritance are extremely well-known and well-documented. Ruby-style mixins and Java interfaces are two attempts to avoid this (note that both languages only support single inheritance) but both of these are also broken. Mixins have ordering problems and interfaces force you to redefine the implementation every time -- even if the implementation doesn't change.

A trait is essentially a group of pure methods that serves as a building block for classes and is a primitive unit of code reuse.

So, can you spell that in Perl? It sounds suspiciously like it's nothing more than a module that allows you to import some methods into your namespace. Or is that a mixin?

That's the basics of it (traits are very simple to use. It's the theory behind traits which throws a few people off).

Traits, however, are far more powerful than mixins. Imagine, for example, that you have two traits: TSpouse and TBomb. Each of those traits provides two methods, set_fuse() and explode(). You want to institute a "FamilyMember" class and use those traits. Naturally, you want to use the TSpouse::explode() method because it's non-lethal (usually). You want to use the TBomb::set_fuse() method because you can control the timing.

If these were mixins, you have a problem. With Ruby, you would get the set_fuse() and explode() methods from whichever of these mixins you used last. If you want one from each, you have to use delegation or something similar. That forces you to write more code and, being lazy, we don't want to do that.

With traits, if there are method conflicts, the traits fail at compile time and you have to explicitly resolve the problem:

package FamilyMember;
use Class::Trait
  TSpouse => { exclude => ['set_fuse'] },
  TBomb   => { exclude => ['explode'] };

And now FamilyMember->can('explode') (from TSpouse) and FamilyMember->can('set_fuse') (from TBomb).

However, if the TSpouse and TBomb traits had no conflicts, it would be as simple as this:

use Class::Trait qw(TSpouse TBomb);

And you have everything you need. Further, because these methods are flattened into the FamilyMember class, Perl doesn't need to search @ISA to find them. That's a nice performance boost to go along with the complexity management.

In a nutshell, the reasoning behind traits is simple. A class needs to provide *everything* you should be able to do with that class. Thus, it needs to be complete. If you think of classes for code reuse benefits (for example, when we inherit from classes), then they should be as small as possible so we don't accidentally pull in more stuff than we want or need. Think "cohesion".

So classes should generally be both complete and small, but these needs often conflict. Since traits are both small and provide code reuse, they satisfy both requirements. Further, unlike Java interfaces, they provide the implementation so you don't have to keep rewriting it. Like Java interfaces, they fail at compile time if you've not used them correctly. Unlike Ruby mixins, they don't have ordering problems.

The compile-time failure is a very important feature. If you forget to test a method which requires another method, you don't want to see the failure three months down the road when the system is in production. Note that this feature is not perfect. If you forget to specify a method a trait requires, due to Perl's dynamic nature Perl has no way of verifying that $self->get_reciept; is misspelled.

Another vitally important feature is that in addition to being able to use traits, you can now query an object to find out its capabilities:

if ( $object->does('TBomb') ) {
  # we know it responds to explode() and set_fuse() messages
}

Or, more realistically:

if ( $employee->does('TManager') ) {
  # we can now safely use management functions
}

Note that Class::Trait supports renaming does() if it conflicts with any of your methods.

I'm still working on Class::Trait and doing some deep internals work with it, but the basic features are there.


While I don't understand it "all"...

sigzero on 2005-11-19T23:58:37

My brain keeps telling me that is a great way to pass methods around. More thought is needed.

Re:While I don't understand it "all"...

chaoticset on 2005-11-20T15:22:31

Echo that -- while I can't mentally reconcile it with a lot of other things I actually do understand, somehow it sounds attractive and handy.

Research need is implied.

Thanks for the Explanation!

Shlomi Fish on 2005-11-20T22:12:38

Hi Ovid!

Thanks for the explanation. I understood everything (or at least hope I did). Traits sound very cool and everything, and I'll keep them in mind if I ever find a need to use them.

Also thanks for working on the module! <hug />

BTW, how are traits related to Perl 6's Roles? Or to Class::Roles? I sort-of was told that they are related on IRC once. I should note that I could not understand what Roles are from the OOP Apocalypse, and no one yet was able to explain to me what they were. (including the Class::Roles POD documentation). I thought I might eventually need to study the code, but maybe now I won't.

Thanks again.

Re:Thanks for the Explanation!

Ovid on 2005-11-20T23:47:43

Traits are pretty much the same thing as Perl 6 roles. However, the original trait descriptions did not allow for the application of traits at runtime. Perl 6 roles do and some of the CPAN implementations for Perl 5 do allow runtime application.

As for how Class::Trait compares with Class::Role, see this comparison (that link also includes a couple of comments about Class::Roles).

It's clear to me now that Class::Trait needs to support runtime trait application, so this is my next major step.