5

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.

Glen Solsberry
  • 11,960
  • 15
  • 69
  • 94
  • possible duplicate of [How to take code reference to constructor?](http://stackoverflow.com/questions/4229562/how-to-take-code-reference-to-constructor) – Eric Strom Jan 26 '11 at 17:37
  • 2
    your problem is not with calling the method, but with storing a reference to it. see the above dup link for how to do this – Eric Strom Jan 26 '11 at 17:45
  • 1
    see also [How do I call a function name that is stored in a hash in Perl?](http://stackoverflow.com/questions/1836178/how-do-i-call-a-function-name-that-is-stored-in-a-hash-in-perl) for the other half (calling the reference once you have stored it), as that part is also incorrect. – Ether Jan 26 '11 at 18:20
  • @ysth: I withdraw my claim; I mistakenly thought that the order of evaluating the hash key would not take precedence over calling the coderef. (The question I linked to had a more complicated example, where order of operations does get in the way.) – Ether Jan 26 '11 at 19:55

3 Answers3

5

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?

Community
  • 1
  • 1
Eric Strom
  • 39,821
  • 2
  • 80
  • 152
  • 1
    "need" is too strong for this. It's one way to do it, but it requires a lot more work and engineering than simpler methods. – brian d foy Jan 26 '11 at 19:05
  • @brian => if you down-voted for "need", that is fairly petty... otherwise, down-voter, please explain? – Eric Strom Jan 26 '11 at 19:31
  • @brian => the answer you gave first was (and still is) incorrect. the second answer you gave tightly couples the hash and the following code with the `$dh`. What if the user wants to handle some urls with a different module, or directly with a code ref. With your tightly coupled example that will not be possible. I'd expect someone with your rep to know that down votes are for incorrect or otherwise unhelpful answers... – Eric Strom Jan 26 '11 at 19:45
3

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;
            }
        }
    }
brian d foy
  • 129,424
  • 31
  • 207
  • 592
  • using `->can` to retrieve the reference is not enough, you need an invocant. see the linked dup question – Eric Strom Jan 26 '11 at 18:53
  • Is `can` a builtin ability of all Modules? – Glen Solsberry Jan 26 '11 at 18:53
  • can comes from UNIVERSAL. That doesn't mean that all modules have the same behavior for can(). Forget that part though because the headache is incidental to doing it the hard way. – brian d foy Jan 26 '11 at 19:02
  • Your new answer not only tightly couples the `$dh` object and its package with the processing loop (making it very difficult to use multiple modules), but also does not fill the need the OP was trying to solve. In the OP's case, they could specify a complex regex like `/(article|blog)/` or `/(article.*)/` to be handled by a common method. With your method, that is no longer possible. And your first answer is still wrong: `It already knows its object`. No it doesn't, and it never did. – Eric Strom Jan 26 '11 at 19:54
  • Down-voting in favor of @EricStrom's answer. – Qwertyzw Sep 08 '18 at 10:09
0

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.

friedo
  • 65,762
  • 16
  • 114
  • 184