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

I want to be able to sort a list in a number of predefined ways. So I wrote something like this:

#! /usr/local/bin/perl -w use strict; sub forward ($$) {$a cmp $b} sub backward ($$) {$b cmp $a} my $sorter = [\&forward, \&backward]; my @list = qw(kholsky stencil dnubietna barkhausen manganese fairing fleische flake schlozhauer gascoigne); my $offset = shift || 0; print "$_\n" for sort {$sorter->[$offset]->()} @list;

I stumbled a bit on the last line. At first I wanted to pass the coderef directly, with:

 sort $sorter->[$offset] @list

But the tokeniser has trouble parsing that. Is there another way to achieve my goal without going through the double layer of code? I'm not really fussed about the performance of the above, but it's always nice to be sure that you're not missing out on something.

update: bonus points to skeeve, that's what I was missing :) Actually, there are a couple more points to take care of: the subs don't get $a and $b automatically (update-update: because of the prototypes, thanks kwaping), and the sorter SUBNAME must be a scalar, not an array reference. Which gives:

#! /usr/local/bin/perl -w use strict; sub forward ($$) {my ($x, $y) = @_; $x cmp $y} sub backward ($$) {my ($x, $y) = @_; $y cmp $x} sub bylen ($$) {my ($x, $y) = @_; length($x) <=> length($y) || $x cmp $y } my @sorter = qw(forward backward bylen); my @list = qw(kholsky stencil dnubietna barkhausen manganese fairing fleische flake schlozhauer gascoigne); my $s = $sorter[shift || 0]; print "$_\n" for sort $s @list;

• another intruder with the mooring in the heart of the Perl

Replies are listed 'Best First'.
Re: Choosing the sort routine on the fly
by Skeeve (Parson) on Sep 08, 2006 at 08:12 UTC
    How about this?
    #! /usr/local/bin/perl -w use strict; sub forward {$a cmp $b} # removed prototype sub backward {$b cmp $a} # removed prototype my @sorter = qw/forward backward/; my @list = qw(kholsky stencil dnubietna barkhausen manganese fairing fleische flake schlozhauer gascoigne); my $offset = shift || 0; my $subname= $sorter[$offset]; print "$_\n" for sort $subname @list;
    Simply supply the sort routine's name.

    Update: Thanks to kwaping for noticing that this doesn't work. In fact I oversaw the word "forward" inside the output. To fix it I needed the 3 changes shown above. Prototype must be removed because If the subroutine's prototype is "($$)", the elements to be compared are passed by reference in @_, as for a normal subroutine.. The aditional $subname is needed because: SUBNAME may be a scalar variable name (unsubscripted), in which case the value provides the name of (or a reference to) the actual subroutine to use.

    s$$([},&%#}/&/]+}%&{})*;#$&&s&&$^X.($'^"%]=\&(|?*{%
    +.+=%;.#_}\&"^"-+%*).}%:##%}={~=~:.")&e&&s""`$''`"e
      I don't think this works. Here's the output I got from your code:
      barkhausen dnubietna fairing flake fleische forward gascoigne kholsky manganese schlozhauer stencil
      As you can see, the word "forward" was simply added to the list to be sorted. If I supply an $offset of 1, the word "backward" is added to the list of sorted words instead.

      ---
      It's all fine and dandy until someone has to look at the code.
Re: Choosing the sort routine on the fly
by hawtin (Prior) on Sep 08, 2006 at 08:05 UTC

    I had a similar problem, I wanted to create a sort function that kept track of which attributes to use, so I used a closure:

    sub sort_fun { # Get a has ref and and sort spec my($hash_ref,$sort) = @_; my @sort_columns = split(/[\|\,\+]/,$sort); # Do some stuff... # Create a closure function return sub { # Test stuff... foreach my $col (@sort_columns) { # lexically() is like an extended version of cmp my $ret = lexically($hash_ref->{$a}->{$col}, $hash_ref->{$b}->{$col}); return $ret if($ret != 0); } return 0; } } my $sort_fun = sort_fun(\%tracks,"year|name"); foreach my $id (sort $sort_fun keys(%tracks)) { # Do something for every $id }

    The hardest part was creating a thing that sort would accept as a function.

Re: Choosing the sort routine on the fly
by BrowserUk (Patriarch) on Sep 08, 2006 at 08:09 UTC

    I think you are being bitten by the optimisation that translates {$a cmp $b} and {$b cmp $a} into direct sorts that do not use a callback. Effectively the block supplied, if one of a few pre-defined forms is used to compile in a different codepath at compile time.

    About the best I've achieved in the past is to have both codepaths in the code and select between them yourself.

    $flag = 1; print $flag ? sort 'a'..'z' : sort {$b cmp $a } 'a'..'z';; a b c d e f g h i j k l m n o p q r s t u v w x y z $flag = 0; print $flag ? sort 'a'..'z' : sort {$b cmp $a } 'a'..'z';; z y x w v u t s r q p o n m l k j i h g f e d c b a

    It means some repetition but avoids a callback and so speeds up the sorting. I've wished several times that there were sortAplhaAscending, sortAlphaDescending, sortNumericAscending & sortNumericDescending keywords in the language. The codepaths already exist in the runtime, but you have to use somewhat obscure syntax to get at them.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      ...being bitten by the optimisation...

      Not in my particular case. The real-life code is more complicated, as I'm sorting arrays of arrays on an arbitrary column, and the comparators are more that just straight string or numeric compares. Hence I have a number of routines that know how to sort the contents of a column, and I associate each column with a sorter.

      • another intruder with the mooring in the heart of the Perl

Re: Choosing the sort routine on the fly
by sgifford (Prior) on Sep 08, 2006 at 16:23 UTC
    This works for me:
    my $s = $sorter->[$offset]; print "$_\n" for sort $s @list;
    It's a bit kludgey, but it works.

    Also, I had to remove the prototypes from forward and backward to get them to work; according to sort's documentation, prototypes cause the parameters to be passed in @_ instead of $a and $b.

      See my updated post above with 2 cites from perldoc -f sort. They explain why you had to remeove the prototype and had to add an aditional variable $s.

      s$$([},&%#}/&/]+}%&{})*;#$&&s&&$^X.($'^"%]=\&(|?*{%
      +.+=%;.#_}\&"^"-+%*).}%:##%}={~=~:.")&e&&s""`$''`"e
Re: Choosing the sort routine on the fly
by Limbic~Region (Chancellor) on Sep 08, 2006 at 12:44 UTC
Re: Choosing the sort routine on the fly
by Outaspace (Scribe) on Sep 08, 2006 at 23:35 UTC
    I use string eval for the sort function. Btw. can anyone tell me if this is evaled forevery comparison ?
    my $szSortFunction = '$a cmp $b'; my @szSortedLines = sort { eval $szSortFunction } @szLinesToSort;
      I can confirm that it is evaled for every comparison.