Class::Std fun and my foolishness

Ovid on 2005-10-16T01:37:11

Ever get embarrassed because as soon as you upload something to the CPAN you remember what you should have done? I just did. Array::AsHash 0.21 is now on its way to the CPAN because I handled the each iterator incorrectly. Previously, I had this:

while (my ($k, $v) = $array_as_hash->each) {
  ...
}

# as code reference
my $each = $array_as_hash->each;
while (my ($k, $v) = $each->()) {
  ...
}

That was really, really stupid because the entire point of this module was to allow much richer interaction than a simple tied hash. For example, let's say someone wants to invert this hash (which is perfectly acceptable because I allow references as keys) and they ask me to provide an invert method. With a tied hash, you're stuck. If I added this method (I will if someone needs it), you would be able to do this:

my $array = Array::AsHash->new({array => [qw/one 1 two 2/]});
print $array->get('one'); # prints '1'
$array->invert;
print $array->get(1);     # prints 'one'

It's useful things like that which make true objects preferable to tied variables. I couldn't do that with the $each iterator because I was returning a code reference and that completely defeats my intent. For example, what if I want to know if I'm on the first instance of the $each iterator? You have to do this:

my $each = $array_as_hash->each;
while (my ($k, $v) = $each->()) {
  if ($array_as_hash->first) {
    ...
  }
  ...
}

However, if you had passed the iterator to another class, you'd also have to pass the Array::AsHash instance, too. That's silly. Yes, you could just pass the Array::AsHash instance, but if all your other class expects is an iterator conforming to a standard iterator interface, you may not want to do that. Now I return an object blessed into the Array::AsHash::Iterator class.

my $iter = $array_as_hash->each;
while (my ($k, $v) = $iter->next) {
  if ($iter->first) {
    ...
  }
  elsif ($iter->last) {
    ...
  }
}
$iter->reset_each; # if you need to reuse it but haven't iterated
                   # over all of the values
my $array_as_hash = $iter->parent; # if you need the original object back

It breaks backwards compatability, but since the Array::AsHash version which returns an iterator object has only been out for less than one day, I'm not too worried about that.

And what does Class::Std have to do with this? Well, the each method looks similar to the following (simplified for clarity):

sub each {
    my $self  = shift;
    my $ident = ident $self;

    my $each = sub {
        my $index = $current_index_for{$ident} || 0;
        my @array = $self->get_array;
        if ( $index >= @array ) {
            # end of iterator.  Reset and return false
            $self->reset_each;
            return;
        }
        my ( $key, $value ) = @array[ $index, $index + 1 ];
        $current_index_for{$ident} += 2;
        return ( $key, $value );
    };
 
    return $each->() if wantarray;

    return Array::AsHash::Iterator->new( {
        parent   => $self,
        iterator => $each,
    } );
}

And that lets me define the iterator class as follows (again simplified for clarity):

package Array::AsHash::Iterator;
use Class::Std;

{
    my %parent_of    :ATTR( :init_arg );
    my %iterator_for :ATTR( :init_arg );

    sub next {
        my $self = shift;
        return $iterator_for{ident $self}->();
    }

    sub first {
        my $self = shift;
        return $parent_of{ident $self}->first;
    }

    sub last {
        my $self = shift;
        return $parent_of{ident $self}->last;
    }

    sub reset_each {
        my $self = shift;
        return $parent_of{ident $self}->reset_each;
    }

    sub parent {
        my $self = shift;
        return $parent_of{ident $self};
    }
}

1;

The beauty of this is how well-encapsulated everything is. These classes could be exceedingly fragile if they were hash-based and someone tried to reach inside in a mistaken attempt to improve performance. Now I have no qualms about rearranging the internals and having complete confidence that I won't break anything. I would almost say "hooray for inside-out objects!", but really, this is what you expect from good OO organization.

Side note: for the time being, if you really need to invert a hash with this module, do this:

my $array = Array::AsHash->new({array => [qw/one 1 two 2/]});
print $array->get('one'); # prints '1'

my @array = reverse $array->get_array;
my $reversed = Array::AsHash->new({array => \@array});
print $reversed->get(1);     # prints 'one'


Re:

Aristotle on 2005-10-16T05:44:31

I don’t understand having ->first and ->last be methods of the main class.

I think you are trying to keep the innards of the iterator separate from the iterator class, right? That would be a worthwhile goal, but as it stands, your design means you can only ever have one iterator at a time per Array::AsHash instance. Someone’s going to get bitten by that at some point.

You could achieve separation on both levels by making two (or three) closures that close over the same variables, of which one is the iterator and the others are first/last state checkers.

Class::Std and mod_perl

Bernhard on 2005-10-16T09:33:06

While encapsulation with Class::Std is nice indeed, it has a downside too. There seem to be non-trivial problem that has to do with the use of CHECK blocks. See http://rt.cpan.org/NoAuth/Bug.html?id=14782.

Re:Class::Std and mod_perl

jdhedden on 2005-11-03T15:56:06

Object::InsideOut is a new module that also provides comprehensive support for inside-out objects. It is faster, more flexible, supports threads including the sharing of objects between threads using threads::shared, and is usable under mod_perl.