Some of you might recall chromatic’s review of Class::Trait, a module which implements traits (PDF) in Perl. He gave a couple of good examples of how one can use traits and I figured a follow-up on one of my many “real world” uses of traits and how it has simplified my code.
I was recently working on a command line program and came upon the following line of code:
@servers = map { $_->{selected} ? $_->{obj} : () } @servers;That is just ugly for a variety of reasons, but here’s why it was created. We were presenting the users with lists and they needed to select one or more items from those lists. When the program was written, only one type of item (a server object) was created for this list. To track whether a server was selected from the list, the following data structure was used.
my $list_item = { obj => $server, selected => 0, };
And that leads to to the above rather ugly map. In fact, the map statement could probably be written more clearly as follows:
@servers = map { $_->{obj} } grep { $_->{selected} } @servers;
That’s easier to read, but it’s still pretty ugly. Also, since we’re using hash keys, they’re really easy to mistype. And on top of that, I now need to reuse this construct for objects of different types so I was looking at potentially having to use that data structure in more than one spot. Now when writing code, having to essentially attach extra state data to objects is quite common. I’ve worked with plenty of classes which have notes() methods or something similar which allow you to shove anything in there you like. For classes which don’t provide this, we do all sorts of tricks like, in this case, wrapping them in data structures which can contain the extra data or separating the objects into different data structures depending on what extra data you want to capture (@selected and @unselected arrays come to mind). Or maybe you pass the objects one at a time to target methods which take extra arguments denoting the metadata. Who knows? Personally, I think traits are a better way to handle this.
All of these techniques have pros and cons, but what I really want is for the object to have that data available, not for me to jump through hoops to preserve that data. I can’t simply build all possible metadata types into all objects which might need them because that’s bloating my code for no reason. Instead, I want a trait which I can apply to a class at runtime and provides just what I need for my code. My first refactoring was a bit of a kludge, but what I have now looks a bit like this:
1 package Trait::Selected; 2 3 use strict; 4 use warnings; 5 6 use Class::Trait 'base'; 7 8 our @REQUIRES = qw(id); 9 10 my $get_id = sub { 11 my $self = shift; 12 return sprintf "%s %d" => ref $self, $self->id; 13 }; 14 15 my %is_selected; 16 17 sub is_selected { 18 my $self = shift; 19 my $id = $self->$get_id; 20 $is_selected{$id} ||= 0; 21 return $is_selected{$id} unless @_; 22 $is_selected{$id} = shift; 23 return $self; 24 } 25 26 1;
Line 6 says “this is a trait”. Line 8 says “any object which uses this trait must supply an id() method”. Line 10 is how you supply truly private methods or functions with traits and line 17 is the actual method that each class will now have.
In my actual code, it’s completely wrong for me to add an is_selected() method to a bunch of classes just because I need them for this one script, so I’m going to apply the trait at runtime with this:
Class::Trait->apply( $some_class, 'Trait::Selected' );Of course, if you don’t want this to apply to all instances of that class, you can apply it just to a single instance by replacing $some_class with an instance of the object in question. (Prototype-based programming, anyone?)
Now that I have that, I can use is_selected() as a getter/setter (accessor/mutator, if you prefer) for this boolean property. So if a particular object from a list I present to the user is selected, I can just do this:
# This used to be $item->{selected} = 1; and is really easy to mistype $object->is_selected(1); # Mistyping the method becomes a runtime fai +lure
And this:
@servers = map { $_->{obj} } grep { $_->{selected} } @servers;
Becomes this:
@servers = grep { $_->is_selected } @serversThat’s much easier to read.
While this made much of my code easier to read and work with, it does show an ugly problem with trying to add runtime traits to languages not designed to support them. What happens when the object goes out of scope? The trait still retains that object’s selected value and it’s tricky to remove. Fortunately for my code, this turns out to not be an issue, but using runtime traits to maintain state is problematic. While Perl 6 doesn’t have this problem, for the time being, I need to solve it for Class::Trait. I have a reasonably workable approach, but it will take some time to implement. Once again, I find myself pining away for Perl 6 for the things that it makes easy.
Cheers,
Ovid
New address of my CGI Course.
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: Maintaining State with Runtime Traits (where)
by tye (Sage) on Oct 10, 2006 at 21:41 UTC | |
by Ovid (Cardinal) on Oct 11, 2006 at 06:01 UTC | |
|
Re: Maintaining State with Runtime Traits
by ysth (Canon) on Oct 11, 2006 at 09:21 UTC | |
|
Re: Maintaining State with Runtime Traits
by jdhedden (Deacon) on Oct 11, 2006 at 19:39 UTC | |
by Ovid (Cardinal) on Oct 11, 2006 at 21:07 UTC | |
|
Re: Maintaining State with Runtime Traits
by diotalevi (Canon) on Oct 10, 2006 at 21:35 UTC | |
by Ovid (Cardinal) on Oct 11, 2006 at 18:04 UTC | |
by diotalevi (Canon) on Oct 14, 2006 at 00:13 UTC | |
|
Re: Maintaining State with Runtime Traits
by xdg (Monsignor) on Oct 11, 2006 at 21:39 UTC | |
|
Re: Maintaining State with Runtime Traits
by roman (Monk) on Oct 13, 2006 at 08:45 UTC |