user2461335
user2461335

Reputation: 57

filter a specific hash tag in perl

I have two subroutines called in order, in the first subroutine I need to filter out a specific key/value of a hash. I tried "delete" and it works where it entirely deletes that option from my hash but I am also referring to the same hash in the second subroutine. I need the key/value not to be referred only in sub1. How can I achieve this? My hash looks similar to this:

my $hash => { a => 123,
          b => 234,
          c => {option1 => 123,
                option2 => 235},
        };

sub1 {
    sub1 needs only hash values a and b

    # the following is what I did.
    my $c = delete($hash->{c});
}

sub2 {
    sub2 needs all a, b and c but after delete on $hash
    I can only see a and b
}

sub1 and sub2 needs to be executed in order, in that case how do I achieve this? Appreciate any help or guidance.

Thanks in advance.

Upvotes: 0

Views: 61

Answers (3)

DavidO
DavidO

Reputation: 13942

This is one of those rare cases where local may be useful to you:

my $hash => { 
    a => 123,
    b => 234,
    c => {
        option1 => 123,
        option2 => 235,
    },
};

sub sub1 {
    # We only need hash values a and b
    delete local $hash->{c};

    print "sub1: $_\n" for sort keys %$hash;
}

sub sub2 {
    # sub2 needs all a, b and c.

    print "sub2: $_\n" for sort keys %$hash;
}

sub1();
sub2();

The output will be

sub1: a
sub1: b
sub2: a
sub2: b
sub2: c

The keyword 'local' localizes the key so that deleting it is undone when the scope exits. The best explanation in Perl's documentation for how this works is perlsub: Localized deletion of elements of composite types.

Another point to make is that it's usually beneficial to pass arguments into a subroutine rather than absorbing them from the surrounding scope:

my $hash = {
    a => 'foo',
    b => 'bar',
    c => {baz => 'bang'},
};

sub sub1 {
    my $href = shift;
    delete local $href->{c};

    print "sub1: $_\n" for sort keys %$href;
}

sub sub2 {
    my $href = shift;
    print "sub2: $_\n" for sort keys %$href;
}

sub1($hash);
sub2($hash);

Even with this change, modification of non-localized elements in $href will propagate back to $hash, since you're passing by reference. But it's still a good habit to get into since it improves readability, and decouples your subroutines from the nomenclature of the surrounding scope, which hopefully contributes to more reusable subs.

Another technique that is occasionally useful is to do a deep clone of the passed-in structure, sot that you are free to modify the clone without having changes propagate back to the referent that was passed into the subroutine. However, this can be expensive (or even impossible) if the elements contain objects rather than plain old values or nested values. Nevertheless, for completeness I'll mention Storable's dclone function, which might be used like this:

use Storable qw(dclone);

my $hash = {
    a => 123,
    b => 234,
    c => {option1 => 123, option2 => 235},
};

sub sub1 {
    my $href = shift;
    my $clone = dclone($href);
    delete $clone->{c};
    print "sub1: $_\n" for sort keys %$clone;
}

sub sub2 {
    my $href = shift;
    print "sub2: $_\n" for sort keys %$href;
}

sub1($hash);
sub2($hash);

dclone is not the only way to clone a data structure. You might write your own solution that is better suited to your specific case, particularly if you know ahead of time the structure's shape and composition. But as a general solution, it's pretty useful.

But ultimately dclone is going to the expense of creating an entirely new data structure, rather than just hiding a key out of the way temporarily which is what delete local $href->{c} does. If you can use local to avoid the need to create a copy of the structure, or to eliminate the potential source of errors and extra thinking that has to go into saving the value away manually (ie, my $temp = delete $href->{c}; ... $href->{c} = $temp;), then you have found one of those times where it turns out to be the right tool for the job.

Upvotes: 3

Craig Estey
Craig Estey

Reputation: 33601

There are two basic ways to do it.

Create a temporary hash for sub1:

my %hash = (
    a => 123,
    b => 234,
    c => { option1 => 123, option2 => 235});

sub1(\%hash);
sub2(\%hash);

sub sub1
{
    my($hash) = @_;
    my(%tmp);

    # this [followed by delete below] is faster than grep, etc.
    %tmp = %$hash;
    $hash = \%tmp;

    # sub1 needs only hash values a and b
    # the following is what I did.
    delete($hash->{c});

    # do stuff ...
}

sub sub2
{
    my($hash) = @_;

    # sub2 needs all a, b and c but after delete on $hash
    # I can only see a and b
}

Or, delete the element in sub1 and add it back before exiting:

my %hash = (
    a => 123,
    b => 234,
    c => { option1 => 123, option2 => 235});

sub1(\%hash);
sub2(\%hash);

sub sub1
{
    my($hash) = @_;

    # sub1 needs only hash values a and b
    # the following is what I did.
    my $c = delete($hash->{c});

    # do stuff ...

    # add it back
    $hash->{c} = $c;
}

sub sub2
{
    my($hash) = @_;

    # sub2 needs all a, b and c but after delete on $hash
    # I can only see a and b
}

Upvotes: 0

darkgrin
darkgrin

Reputation: 580

I think what you want is something like this:

sub sub1 {
    my %new_hash;
    my @keys = grep { $_ ne 'c' } keys %$hash;
    @new_hash{@keys} = @$hash{@keys};
    return \%new_hash;
}

Also, look at Looking for a more efficient way to filter out a perl hash

Upvotes: 1

Related Questions