views:

147

answers:

1

I'm trying to write some abstract code for searching through a list of similar objects for the first one whose attributes match specific values. In order to do this, I need to call a bunch of accessor methods and check all their values one by one. I'd like to use an abstraction like this:

sub verify_attribute {
    my ($object, $attribute_method, $wanted_value) = @_;
    if ( call_method($object, $attribute_method) ~~ $wanted_value ) {
        return 1;
    }
    else {
        return;
    }
}

Then I can loop through a hash whose keys are accessor method names and whose values are the values I'm looking for for those attributes. For example, if that hash is called %wanted, I might use code like this to find the object I want:

my $found_object;
FINDOBJ: foreach my $obj (@list_of_objects) {
    foreach my $accessor (keys %wanted) {
        next FINDOBJ unless verify_attribute($obj, $accessor, $wanted{$accessor});
    }
    # All attrs verified
    $found_object = $obj;
    last FINDOBJ;
}

Of course, the only problem is that call_method does not exsit. Or does it? How can I call a method if I have a string containing its name? Or is there a better solution to this whole problem?

+5  A: 
my $found_object;
FINDOBJ: foreach my $obj (@list_of_objects) {
  foreach my $accessor (keys %wanted) {
    next FINDOBJ unless $obj->$accessor() == $wanted{$accessor};
  }
  # All attrs verified
  $found_object = $obj;
  last;
}

Yes, you can call methods this way. No string (or any other) eval involved. Also, substitute == with eq or =~ depending on the type of the data...

Or, for some extra credits, do it the functional way: (all() should really be part of List::Util!)

use List::Util 'first';

sub all (&@) {
  my $code = shift;
  $code->($_) || return 0 for @_;
  return 1;
}

my $match = first {
                    my $obj = $_;
                    all { $obj->$_ == $attrs{$_} }
                      keys %wanted
                  } @list_of_objects;

Update: Admittedly, the first solution is the less obfuscated one, so it's preferable. But as somebody answering questions, you have add a little sugar to make it interesting for yourself, too! ;-)

tsee
Yeah, I just figured this out a few seoncds ago by trial and error. Cool.
Ryan Thompson
I just use `List::AllUtils`. Or `Util::Any qw( :all )`. But in my example code I try to stick to the core.
Ryan Thompson
I believe that for this to work you must disable strictness checks with `no strict;` (not necessarily globally) -- correct?
j_random_hacker
@j_random_hacker: For $obj->$method? Nope. Doesn't interfere with "use strict" at all. It's not a symbolic reference if that's what you#re thinking.
tsee
@tsee, thanks, that was what I was thinking. In that case this is something I haven't come across before -- could you point me to where it's described in the Perl docs? Thanks!
j_random_hacker
@j_random_hacker: Not sure where it's documented in the references, but "perldoc perltoot" mentions the syntax.
tsee
j_random_hacker
Actually this page has some interesting discussion, although the Google Groups links seem to be down: http://www.perlmonks.org/index.pl?node_id=139254. My conclusion: no-one really knows the answer.
j_random_hacker
Eric Strom
Good point Eric. Hmmm, mind you `__PACKAGE__->can($func)->()` will let you call a function by name under `use strict;`, which makes me wonder why `$func->()` isn't just syntactic sugar for that as well. Except it doesn't work for builtins apparently. New conclusion: Aargh.
j_random_hacker
@j_random_hacker: I'm actually quite happy it doesn't work for builtins. Your example should have read `__PACKAGE__->can($methodname)->()`. The builtins are not methods. Regarding why `$func->()` doesn't work that way: $class->can($methname) returns a code reference. Thus it's actually the other way around. Executing the result of `can()` uses `$func->()`.
tsee
Eric's comment was a justification of why `$obj->$method()` works under `use strict` while `$func->()` does not; I was just trying to point out that his justification applies nearly as well to the 2nd case as to the 1st. But I see now I was wrong, because of course `__PACKAGE__->can($func)->()` will pass the current package as parameter 1 so it's not a "plain" function call. Since there is a `can()` for objects/classes, I don't know why there isn't a corresponding `can_func()` for plain packages (or is there?)
j_random_hacker