loris has asked for the wisdom of the Perl Monks concerning the following question:

Hello Robed-Ones,

Here's the problem:

I have an array of objects, let's say Monks. I want to sort them into groups, let's say according to Level, and then call a subroutine, e.g. callToPrayer, which takes an array of monks as its argument, on each group individually. What would be the easiest/most elegant way grouping the monks?

I have had a look at the chapter in the Perl Cookbook about Hashes with Multiple Values Per Key, but I wondered if there was another way without using references.

I would be very grateful for any advice.

loris

Replies are listed 'Best First'.
Re: Grouping Objects by Attribute
by edan (Curate) on Nov 01, 2004 at 09:02 UTC

    some untested pseudo-code ;) to get you started...

    my %monks_by_level; for my $monk ( @array_of_monks ) { my $level = $monk->level(); # or ->{level} or whatever... push @{ $monks_by_level{$level} }, $monk; } for my $level ( keys %monks_by_level ) { print "doing $level...\"; callToPrayer( $monks_by_level{$level} ); } sub callToPrayer { my ($monks) = @_; for my $monk ( @$monks ) { print "$monk->name() here!\n"; } }
    --
    edan

      Thanks, edan, that did the trick.

      However, I don't quite get the bit

      @{ $monks_by_level{$level} }

      I understand this to mean that the $monks_by_level{$level} is the value of the hash for the key $level and that the @{ ... } around it automagically makes this value an array. Is this right?

      loris

        You're right, you just need a bit of terminology thrown in there. $monks_by_level{$level} is an array reference (see perlref for details) and @{ } is how we dereference an array reference, so that we can use it as an array. You could also dereference individual scalars in the array using syntax such as $monks_by_level{$level}[0].

        --
        edan

Re: Grouping Objects by Attribute
by TedPride (Priest) on Nov 01, 2004 at 09:20 UTC
    Since there's a finite, sequential number of levels, you might as well just use nested arrays:
    my (@monks); while (<DATA>) { chomp; split(/ /); push(@{$monks[$_[1]]}, $_[0]); } mysub($_) for @monks; sub mysub { print join(' ', sort(@$_)) . "\n"; } __DATA__ monk1 2 monk2 5 monk3 8 monk4 4 monk5 1 monk6 5 monk7 3 monk8 7 monk9 7
    Or array / hash, if you need to be able to do more than just run through each set of monks:
    my (@monks); while (<DATA>) { chomp; split(/ /); $monks[$_[1]]{$_[0]} = (); } mysub($_) for @monks; sub mysub { print join(' ', sort(keys %$_)) . "\n"; } __DATA__ monk1 2 monk2 5 monk3 8 monk4 4 monk5 1 monk6 5 monk7 3 monk8 7 monk9 7
    A hash would enable you to quickly check for specific monks inside each set.
Re: Grouping Objects by Attribute
by dragonchild (Archbishop) on Nov 01, 2004 at 13:39 UTC
    ... but I wondered if there was another way without using references.

    As you've received excellent answers above, this is a general comment. You're basically asking if you can drive a car without a rearview mirror. Well, yes, you can. But, it's a really dumb way to drive a car.

    References are an integral part of Perl5, and should be treated as another tool in your toolbox, not as something to be feared and shunned. They are the way to create multi-level data structures1. Period.

    The Perl Cookbook generally has the best-of-breed way of doing task XYZ. It's the reason it's the cookbook. So, I'd trust it.

    Now, I'm not saying "Don't question the wisdom", by any means. But, for most questions, you'll get the answer "The wisdom is right." *grins*

    1. Well, there are other ways, but you really don't want to go there. References were put into the language for a really darn good reason. Globbing your data structures a la Perl4 just sucks.

    Being right, does not endow the right to be rude; politeness costs nothing.
    Being unknowing, is not the same as being stupid.
    Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
    Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

Re: Grouping Objects by Attribute
by Anonymous Monk on Nov 01, 2004 at 10:33 UTC
    I'm a bit confused about your "call a subroutine, e.g. callToPrayer, which takes an array of monks as its argument, on each group individually." in combination with "without using references". The only way to pass in an array to a subroutine is by passing a reference to the array. After all, all a subroutine can take is a (possible empty) list (of scalars).

    I'd do something like:

    my @monks = ...; # Array of Monk objects, with a level method. my %monks_by_level; push(@{$monks_by_level {$_->level()}}, $_) for @monks; while (my ($level, $monks) = each(%monks_by_level)) { callToPrayer($monks) }

    If you settle for passing in a list (and hence, losing any reference (no pun intended) to an array), you can do it "without references" (well, the objects you start out with are references) - if you know which levels there are (but you could find out by making another pass):

    foreach my $level (1 .. 10) { callToPrayer(grep($_->level() == $level, @monks)); }
    The first code fragment needs only one pass over the array of objects - the last fragment needs a pass for each level.