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

I need to be able to sort a hash by values, but i cannot inline the sort function, because it will not be specified until runtime. So i cant use the usual:
foreach my $key ( sort { $hash{$a} cmp $hash{$b} } keys %hash ) { # do something useful }
sort of thing.

I will need to use a named sort function, specified at runtime, but something like this will not work:

foreach my $key ( sort my_sorter keys %hash ) { # do something useful } ... ... sub my_sorter { return $hash{$a} cmp $hash{$b}; }
Because %hash is not necessarily scoped in my_sorter()

In my situation, it will be nearly impossible to have the hash being sorted in the proper scope directly.

What i realy want to do is pass anonymous sort subroutines into the function that needs to sort the hash, but i just dont see how to make this work properly. The hash itself will always be out of scope.

Note that this is a simplification of the real problem, the sorting by values problem is only part of the situation, but its the only part thats giving me trouble.

So i guess the real issue is: How do i sort by values without an inlined sort?

Thanks much - beating my head

Replies are listed 'Best First'.
Re: Sort by value - new twist
by ikegami (Patriarch) on Sep 01, 2004 at 18:59 UTC

    First, the following allow you to switch the procedure being called:

    $sorter = \&my_sorter; foreach my $key (sort { &$sorter } keys %hash ) { # do something useful }

    Update:

    Because %hash is not necessarily scoped in my_sorter

    Option 1

    $sorter = \&my_sorter; foreach my $key (sort { &$sorter(\%hash) } keys %hash ) { # do something useful } sub my_sorter { my ($hashref) = @_; return $hashref->{$a} cmp $hashref->{$b}; }

    Option 2

    $sorter = \&my_sorter; foreach my $key ( map { $_->[0] } sort { &$sorter } map { [ $_, $hash{$_} ] } keys %hash ) { # do something useful } sub my_sorter { return $a->[1] cmp $b->[1]; }

    Option 3

    Use local %hash = %the_hash_to_process; before the foreach, and our %hash inside my_sorter.

    There are more ways.

      Option 1 does it exactly!!!

      Brilliant!

        shemp,
        This is also how Tie::Hash::Sorted is able to dynamically change the sort routine. It can even sort via lexicals that the package normally wouldn't even be able to see via closures. If you could deal with your hash being tied, Tie::Hash::Sorted gives you the ability to define your sort routine at runtime for free.

        Cheers - L~R

      Regarding all options you propose: you need to handle that $a and $b is a package global if you want to have your sort routines elsewhere. IMHO, the easiest way to handle this is simply to pass them along in the function call. I.e. for option 1:

      my $sorter = \&my_sorter; foreach my $key (sort { $sorter->(\%hash, $a, $b) } keys %hash) { # do something useful } sub my_sorter { my ($hashref, $a, $b) = @_; return $hashref->{$a} cmp $hashref->{$b}; }

      You can do it by using caller and symbolic links, but I don't see any real benefit by that.

      ihb

      Read argumentation in its context!

Re: Sort by value - new twist
by Aristotle (Chancellor) on Sep 01, 2004 at 19:16 UTC

    Enter functional programming techniques.

    sub by_hashval_func { my ( $hashref ) = @_; return sub { $hashref->{ $a } cmp $hashref->{ $b } }; } foreach my $key ( sort by_hashval_func( \%hash ) keys %hash ) { # do something useful }

    Update: I just remembered that $a and $b are package globals, ie the above will not work if by_hashval_func is in a different package than the sort call ultimately using the closure. To cater to that case, the prototyped sort block form is necessary:

    sub by_hashval_func { my ( $hashref ) = @_; return sub($$) { $hashref->{ $_[0] } cmp $hashref->{ $_[1] } }; }

    Makeshifts last the longest.

Re: Sort by value - new twist
by demerphq (Chancellor) on Sep 01, 2004 at 19:30 UTC

    Update: The following is an example of a hasty post that was written without properly consider either the question or the other replies. It only answers the general case and not shemp's question. Ive reanswered below with something slightly more useful.

      Make the inlined sort routine take two args like this:

      my $sorter=sub { $_[0] <=> $_[1] };

      and then call it like this:

      my @sorted=sort { $sorter->($a,$b) } @unsorted;

      At least thats what I would do. If that was what actually needed doing.

    Heres what i should have said:

    Make the inlined sort routine take three args like this:

    my $sorter=sub { $_[0]->{$_[1]} <=> $_[0]->{$_[2]} };

    and then call it like this:

    my @sorted=sort { $sorter->($hashref,$a,$b) } @unsorted;

    I have a feeling that the answers that use $a and $b will have problems when the $sorter is defined in a different package. Im not sure about that thou. Anyway in my own code id probably write it as above both to be simple and to avoid having to hunt down the docs on the scoping rules to be sure. :-)


    ---
    demerphq

      First they ignore you, then they laugh at you, then they fight you, then you win.
      -- Gandhi


      Make the inlined sort routine take three args like this:

      This suggestion is effectively equivalent to mine. I just applied what is essentially currying to simplify things.

      Makeshifts last the longest.

        Well actually you hadn't posted the updated version when I posted that. Your original wouldn't work if the sort function was declared in a different package scope. (As I suspected and as you realized :-)


        ---
        demerphq

          First they ignore you, then they laugh at you, then they fight you, then you win.
          -- Gandhi


Re: Sort by value - new twist
by yosefm (Friar) on Sep 01, 2004 at 19:34 UTC
Re: Sort by value - new twist
by VSarkiss (Monsignor) on Sep 01, 2004 at 19:11 UTC

    This is untested, but it seems like you should be able to do something like:

    sub mysort { $_[0]->{$a} cmp $_[0]->{$b} } # ... later ... for my $key (sort mysort(\%hash), keys %hash) { # and so on
    In other words, pass a reference to the hash to the sort function.

      This ends up with some weird behaviour. I'm not exactly sure whats happening, but i dont think the param \%hash is being passed to the sorter in a manner that we (I) think it is.

      Try this:

      my %hash = ( 1 => 'A', 2 => 'B', 3 => 'C', ); foreach my $key (sort mysort(\%hash), keys %hash) { print "$key\n"; } sub mysort { print "mysort: \$_[0] = $_[0] ::: a = $a ::: b = $b\n"; return $_[0]->{$a} cmp $_[0]->{$b}; }
      ...lots of warnings and unexpected things.