tags:

views:

89

answers:

2

Tie::Hash has these:

sub FIRSTKEY { my $a = scalar keys %{$_[0]}; each %{$_[0]} }
sub NEXTKEY  { each %{$_[0]} }

NEXTKEY takes two arguments, one of which is the last key but that arg is never referenced?

The various Tie docs don't shed any light on this other than this in perltie:

my $a = keys %{$self->{LIST}};      # reset each() iterator

looking at the doc for each doesn't add to this.

What's going on?

+9  A: 

You only need to worry about the second argument to NEXTKEY if you care about which key was accessed last. By default, hashes don't care about the order, so it is not used.

As for the second part, the keys function in scalar context returns the number of items in the hash. Any call to keys resets the iterator used by keys and each because it exhausts the iterator.

A call to keys is really a call to FIRSTKEY and calls to NEXTKEY until there are no more items left in that haven't been returned.

A call to each is a call to FIRSTKEY (if FIRSTKEY hasn't been called yet) or a call to NEXTKEY (if FIRSTKEY has been called).

#!/usr/bin/perl

use strict;
use warnings;

my $i = 0;
tie my %h, "HASH::Sorted", map { $_ => $i++ } "a" .. "g";

for my $key (keys %h) {
    print "$key => $h{$key}\n";
}
print "\n";

my $first = each %h;
print "first $first => $h{$first}\n";

my ($second_key, $second_value) = each %h;
print "second $second_key => $second_value\n";

print "\nall of them again:\n";
for my $key (keys %h) {
    print "$key => $h{$key}\n";
}

package HASH::Sorted;

sub TIEHASH {
    my $class = shift;

    return bless { _hash => { @_ } }, $class;
}

sub FETCH {
    my ($self, $key) = @_;

    return $self->{_hash}{$key};
}

sub STORE {
    my ($self, $key, $value) = @_;

    return $self->{_hash}{$key} = $value;
}

sub DELETE {
    my ($self, $key) = @_;

    return delete $self->{_hash}{$key};
}

sub CLEAR {
    my $self = shift;

    %{$self->{_hash}} = ();
}

sub EXISTS {
    my ($self, $key) = @_;

    return exists $self->{_hash}{$key};
}

sub FIRSTKEY {
    my $self = shift;

    #build iterator     
    $self->{_list} = [ sort keys %{$self->{_hash}} ];    

    return $self->NEXTKEY;
}

sub NEXTKEY {
    my $self = shift;

    return shift @{$self->{_list}};
}

sub SCALAR {
    my $self = shift;
    return scalar %{$self->{_hash}};
}
Chas. Owens
@Chas, under what circumstances is NEXTKEY called in an array context? I've never seen logic for a multi-value return from that method.
pilcrow
@pilcrow It isn't, I mistakenly assumed that `my ($k, $v) = each %h;` would call `FIRSTKEY` and `NEXTKEY` in list context, apparently it calls `FETCH` to get the value. I will change the code.
Chas. Owens
@Chas, whew, thanks. You turned my understanding of perltie upside-down there for a moment. :)
pilcrow
Can you comment on how to handle multiple simultaneous loops over the same object. There is only one _list
mmccoo
@mmccoo Perl's default hashes don't support multiple iterators, but it is possible to do, but you must use a custom method, I will create a second answer with one solution.
Chas. Owens
A: 

This one uses a custom each method to allow you to iterate over the sorted hash more than one time. All of the standard rules about not being allowed to add or remove keys are still in effect though. It would be trivial to add a warning that iterators were still in use on a call to STORE or DELETE.

#!/usr/bin/perl

use strict;
use warnings;

my $i = 0;
tie my %h, "HASH::Sorted", map { $_ => $i++ } "a" .. "g";

for my $key (keys %h) {
    print "$key => $h{$key}\n";
}
print "\n";

my $first = each %h;
print "first $first => $h{$first}\n";

my ($second_key, $second_value) = each %h;
print "second $second_key => $second_value\n";

print "\nall of them again:\n";
for my $key (keys %h) {
    print "$key => $h{$key}\n";
}

print "\nmultiple iterators\n";

my $o = tied %h;
while (my ($k, $v) = $o->each("outer")) {
    print "$k => $v\n";

    while (my ($k, $v) = $o->each("inner")) {
        print "\t$k => $v\n";
    }
}

print "\nhybrid solution\n";
while (my ($k, $v) = each %h) {
    print "$k => $v\n";

    #the iter_name is an empty string
    while (my ($k, $v) = $o->each) {
        print "\t$k => $v\n";
    }
}


package HASH::Sorted;

sub each {
    my ($self, $iter_name) = (@_, "DEFAULT");

    #each has not been called yet for this iter
    unless (exists $self->{_iters}{$iter_name}) {
        $self->{_iters}{$iter_name} = [ sort keys %{$self->{_hash}} ];
    }

    #end of list
    unless (@{$self->{_iters}{$iter_name}}) {
        delete $self->{_iters}{$iter_name};
        return;
    }

    my $key = shift @{$self->{_iters}{$iter_name}};

    if (wantarray) {
        return $key, $self->{_hash}{$key};
    }

    return $key;
}

sub TIEHASH {
    my $class = shift;

    return bless {
        _hash => { @_ },
        _iters => {},
    }, $class;
}

sub FETCH {
    my ($self, $key) = @_;

    return $self->{_hash}{$key};
}

sub STORE {
    my ($self, $key, $value) = @_;

    return $self->{_hash}{$key} = $value;
}

sub DELETE {
    my ($self, $key) = @_;

    return delete $self->{_hash}{$key};
}

sub CLEAR {
    my $self = shift;

    %{$self->{_hash}} = ();
}

sub EXISTS {
    my ($self, $key) = @_;

    return exists $self->{_hash}{$key};
}

sub FIRSTKEY {
    my $self = shift;

    #build iterator     
    $self->{_list} = [ sort keys %{$self->{_hash}} ];    

    return $self->NEXTKEY;
}

sub NEXTKEY {
    my $self = shift;

    return shift @{$self->{_list}};
}

sub SCALAR {
    my $self = shift;
    return scalar %{$self->{_hash}};
}
Chas. Owens
@Chas. Thanks for the follow up. It seems there should be a way to do this without deviating from the normal looping syntax. One question that comes to mind is how does 'each' store it's state from iteration to the next? each seems to have the ability to 'yield' that the rest of perl does not
mmccoo
@mmccoo See `hv_iternext` in hv.c in the `perl` source code or in `perlapi` (http://perldoc.perl.org/perlapi.html#hv_iternext).
Chas. Owens
@mmccoo without giving `each` extra information, how would it know which iterator to use? The normal thing to do when you want to iterate over the same hash at different rates is to use `keys` to get the list of keys and iterate over that instead. Out of curiosity, why do you want to be able to have two hash iterators?
Chas. Owens
@mmccoo Schwern is looking into making `each` capable of iterating more than one thing at a time. The tentative plan is to use `Devel::Declare` to modify every `each` function into `safe_each UNIQUE_ID`. Take a look here: http://github.com/schwern/perl5i/issues/issue/142
Chas. Owens