tags:

views:

261

answers:

3

So I have a Perl class. It has a sort() method, and I want it to be more or less identical to the built-in sort() function:

$object->sort(sub ($$) { $_[0] <=> $_[1] });

But I can't do:

$object->sort(sub { $a <=> $b });

Because of scoping. But the List::Util module does this with reduce(). I looked at the List::Util module, and they do some rather nasty things with no strict 'vars' to make this happen. I tried that, but to no avail.

It is my understanding that reduce() works the way it does because it is exported into the appropriate namespace, and thus my class can't do this since the function is quite firmly in another namespace. Is this correct, or is there some (undoubtedly more hideous and ill-advised) way to do this in my situation?

+1  A: 

You can use the local operator to set values for $a and $b for the duration of the subroutine call:

sub sample
{
    my $callback = shift;
    for (my $i = 0; $i < @_; $i += 2) {
        local ($a, $b) = @_[$i, $i + 1];
        $callback->();
    }
}       

sample sub { print "$a $b\n" }, qw(a b c d e f g h i j);

If you have an ordinary subroutine, rather than a method, then you can make it be even more like sort, so you don't need to use sub before your callback function. Use a prototype on the function:

sub sample (&@)

Then you call call it like this:

sample { print "$a $b\n" } qw(a b c d e f g h i j);

Methods, though, are not influenced by prototypes.

Rob Kennedy
He is specifically asking about a `method`, not a plain sub.
Sinan Ünür
That won't work if you call the method from outside the class. You're localizing the *class's* $a and $b, not the caller's.
cjm
Ah. I thought I'd seen this done, but I guess not. My impression from perlvar was that `$a` and `$b` were magic enough that they "just work" in this situation.
Rob Kennedy
No, $a and $b are just normal package variables.
cjm
@Rob: Yes that's what I thought too -- but when I checked, `perlvar` does in fact say they are package variables.
j_random_hacker
+3  A: 

You could use Sub::Identify to find out the package (which it calls stash_name) associated with the coderef. Then set $a and $b in that package as required. You may need to use no strict 'refs' in your method to get that to work.

Here's Evee's answer modified to work in the general case:

use strict;
use warnings;

package Foo;

use Sub::Identify 'stash_name';

sub sort {
    my ($self, $sub) = @_;

    my $pkg = stash_name($sub);
    my @x = qw(1 6 39 2 5);
    print "@x\n";
    {
        no strict 'refs';
        @x = sort {
            local (${$pkg.'::a'}, ${$pkg.'::b'}) = ($a, $b);
            $sub->();
        } @x;
    }
    print "@x\n";

    return;
}


package Sorter;

sub compare { $a <=> $b }

package main;

use strict;
use warnings;

my $foo = {};
bless $foo, 'Foo';

$foo->sort(\&Sorter::compare );

$foo->sort(sub { $b <=> $a });
cjm
List::Util just uses `caller`. I suppose that's not sufficient, in the general case, is it? If the caller passes a function from some other package, then List::Util would set the caller's `$a` rather than the function's.
Rob Kennedy
The version of List::Util in 5.10.1 uses XS code that basically does the same thing Sub::Identify does to figure out the package the coderef belongs to.
cjm
Alternatively, I could rewrite my module in XS, thus losing a dependency _and_ giving me a chance to learn XS. However, in the meantime, I'll look into this.
Chris Lutz
+8  A: 

Well, the other two answers are both half-right. Here's a working solution that actually sorts:

package Foo;

use strict;
use warnings;

sub sort {
    my ($self, $sub) = @_;

    my ($pkg) = caller;
    my @x = qw(1 6 39 2 5);
    print "@x\n";
    {
        no strict 'refs';
        @x = sort {
            local (${$pkg.'::a'}, ${$pkg.'::b'}) = ($a, $b);
            $sub->();
        } @x;
    }
    print "@x\n";

    return;
}


package main;

use strict;
use warnings;

my $foo = {};
bless $foo, 'Foo';

$foo->sort(sub { $a <=> $b });
# 1 6 39 2 5
# 1 2 5 6 39

Presumably you'd sort some data that's actually part of the object.

You need the caller magic so you're localizing $a and $b in the caller's package, which is where Perl is going to look for them. It's creating global variables that only exist while that sub is being called.

Note that you will get a 'name used only once' with warnings; I'm sure there's some hoops you can jump through to avoid this, somehow.

Eevee
That may be good enough for your purposes, but it's fragile. There's no guarantee that the comparison function belongs to the same package as the caller of the `sort` method. That's where Sub::Identify comes in.
cjm
@cjm - This is true, and I will definitely look into Sub::Identify, but my bigger problem is making it work at all, rather than making it work in the general case. Specific solutions are better than general failures. However, combining this answer with yours would give me a general solution, which is a good thing.
Chris Lutz
Although it turns out the `sort` builtin has the same problem. It assumes that the comparison function comes from the same package as the caller. So if you can live with that, you save a dependency on Sub::Identify. (Or you could conditionally require Sub::Identify, and fall back to `caller` if it's not installed. But that's more work.)
cjm
Actually, that's exactly what I've been considering, and I think I'm going to do it.
Chris Lutz
Aha, I never thought of that. Although I'm not sure such a thing should necessarily be encouraged.
Eevee