Glen Solsberry
Glen Solsberry

Reputation: 12320

Using a variable as a method name in Perl

I have a perl script (simplified) like so:

my $dh = Stats::Datahandler->new(); ### homebrew module

my %url_map = (
    '/(article|blog)/' => \$dh->articleDataHandler,
    '/video/' => \$dh->nullDataHandler,
); 

Essentially, I'm going to loop through %url_map, and if the current URL matches a key, I want to call the function pointed to by the value of that key:

foreach my $key (keys %url_map) {
    if ($url =~ m{$key}) {
        $url_map{$key}($url, $visits, $idsite);
        $mapped = 1;
        last;
    }
}

But I'm getting the message:

Can't use string ("/article/") as a subroutine ref while "strict refs" in use at ./test.pl line 236.

Line 236 happens to be the line $url_map{$key}($url, $visits, $idsite);.

I've done similar things in the past, but I'm usually doing it without parameters to the function, and without using a module.

Upvotes: 5

Views: 3371

Answers (3)

Eric Strom
Eric Strom

Reputation: 40142

Since this is being answered here despite being a dup, I may as well post the right answer:

What you need to do is store a code reference as the values in your hash. To get a code reference to a method, you can use the UNIVERSAL::can method of all objects. However, this is not enough as the method needs to be passed an invocant. So it is clearest to skip ->can and just write it this way:

my %url_map = (
    '/(article|blog)/' => sub {$dh->articleDataHandler(@_)},
    '/video/'          => sub {$dh->nullDataHandler(@_)},
); 

This technique will store code references in the hash that when called with arguments, will in turn call the appropriate methods with those arguments.

This answer omits an important consideration, and that is making sure that caller works correctly in the methods. If you need this, please see the question I linked to above:

How to take code reference to constructor?

Upvotes: 5

brian d foy
brian d foy

Reputation: 132802

You're overthinking the problem. Figure out the string between the two forward slashes, then look up the method name (not reference) in a hash. You can use a scalar variable as a method name in Perl; the value becomes the method you actually call:

 %url_map = (
      'foo' => 'foo_method',
      );

 my( $type ) = $url =~ m|\A/(.*?)/|;
 my $method = $url_map{$type} or die '...';
 $dh->$method( @args );

Try to get rid of any loops where most of the iterations are useless to you. :)


my previous answer, which I don't like even though it's closer to the problem

You can get a reference to a method on a particular object with can (unless you've implemented it yourself to do otherwise):

my $dh = Stats::Datahandler->new(); ### homebrew module

my %url_map = (
   '/(article|blog)/' => $dh->can( 'articleDataHandler' ),
   '/video/'          => $dh->can( 'nullDataHandler' ),
);

The way you have calls the method and takes a reference to the result. That's not what you want for deferred action.

Now, once you have that, you call it as a normal subroutine dereference, not a method call. It already knows its object:

BEGIN {
package Foo;

sub new { bless {}, $_[0] }
sub cat { print "cat is $_[0]!\n"; }
sub dog { print "dog is $_[0]!\n"; }
}

my $foo = Foo->new;

my %hash = (
    'cat' => $foo->can( 'cat' ),
    'dog' => $foo->can( 'dog' ),
    );

my @tries = qw( cat dog catbird dogberg dogberry );

foreach my $try ( @tries ) {
    print "Trying $try\n";
    foreach my $key ( keys %hash ) {
    print "\tTrying $key\n";
        if ($try =~ m{$key}) {
            $hash{$key}->($try);
            last;
            }
        }
    }

Upvotes: 3

friedo
friedo

Reputation: 66967

The best way to handle this is to wrap your method calls in an anonymous subroutine, which you can invoke later. You can also use the qr operator to store proper regexes to avoid the awkwardness of interpolating patterns into things. For example,

my @url_map = ( 
    { regex    => qr{/(article|blog)/},
      method   => sub { $dh->articleDataHandler }
    },
    { regex    => qr{/video/},
      method   => sub { $dh->nullDataHandler }
    }
);

Then run through it like this:

foreach my $map( @url_map ) { 
    if ( $url =~ $map->{regex} ) { 
        $map->{method}->();
        $mapped = 1;
        last;
    }
}

This approach uses an array of hashes rather than a flat hash, so each regex can be associated with an anonymous sub ref that contains the code to execute. The ->() syntax dereferences the sub ref and invokes it. You can also pass parameters to the sub ref and they'll be visible in @_ within the sub's block. You can use this to invoke the method with parameters if you want.

Upvotes: 0

Related Questions