public
Last active

Lexically scoping method calls for a specific class in Perl.

  • Download Gist
An-Explanation.pod
Perl

GOAL

Lexically Scoped Methods for a specific class (aiming at PDL).

Why?

In PDL, we have a lot of methods that only make sense for certain analyses, yet when we use a PDL module that defines a set of functions, the module almost always imports its functions into the PDL package. For example, it is rarely useful to apply a one-dimensional Fourier transform to an image, but if any of my code says use PDL::FFT, I can suddenly do so anywhere else in my code.

The Perl world seems to be moving more and more towards lexically scoping a set of ideas or concepts. Lexically scoped variables was a first and huge step in this direction. We have recently seen (with Perl 5.18) the introduction of lexically scoped subroutines. We can then ask: does it make sense to lexically scope PDL functionality?

I don't know if the correct answer is "yes" or "no," but I hope that this working code example gives us some room to play with the idea. The basic idea is that Bar.pm implements a method for the Foo class, and it exposes that method to the Foo class only in the lexical scope(s) where it is used. The machinery necessary for this lexical scoping requires a lexical-method-mapping API for the Foo class, as well as the necessary code to find and invoke lexically scoped methods. The Bar.pm module then simply uses the API to add its methods.

Work in progress

I have been told that the autoloading code in Foo.pm may be overengineered. In particular, I was told that AUTOLOAD() is only invoked after method resolution fails, so checking if the superclass can do the requested method is a waste of time.

It is somewhat annoying to have to write a custom import() and unimport() method for each class that will use this approach. This can be solved by writing a PDL::Exorter::Lexical, which could be used to perform the proper lexical handling using package globals defined in the Bar (or similar) package.

How do I run this thing?

To run this, download Bar.pm, Foo.pm, and lexical-methods.pl into the same folder and execute the Perl script.

Bar.pm
Perl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
use strict;
use warnings;
use 5.010;
 
package Bar;
 
# Installs a method called bar where Foo's autoloader knows where to find it
sub import {
$^H{"Foo/bar"} = 'Bar/bar';
}
 
sub unimport {
delete $^H{"Foo/bar"};
}
 
sub bar {
print "Calling method bar from package Bar\n";
}
 
1;
Foo.pm
Perl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
use strict;
use warnings;
use 5.010;
 
package Foo;
use Carp 'croak';
 
sub new {
my $class = shift;
$class = ref($class) || $class;
return bless {}, $class;
}
 
sub foo {
print "Calling method foo from package Foo\n";
}
 
sub AUTOLOAD {
my $self = $_[0];
 
# Get the called method name and trim off the fully-qualified part
( my $method = our $AUTOLOAD ) =~ s{.*::}{};
 
# Get the hints hash for the calling lexical scope
my $hinthash = (caller(0))[10];
if (exists $hinthash->{"Foo/$method"}) {
my ($package, $package_method)
= split '/', $hinthash->{"Foo/$method"}, 2;
 
# Retrieve the subref and goto() it
my $subref = $package->can($package_method)
or croak("Lexically scoped Foo method $method points to a nonexistent method ${package}::$package_method");
goto &$subref;
}
elsif (my $super_method = $self->SUPER::can($method)) {
goto &$super_method;
}
elsif ($method eq 'DESTROY') {
# Do nothing if we come to this.
return;
}
else {
croak("Can't locate object method \"$method\" via package \"" . __PACKAGE__ . '"');
}
}
 
sub can {
my ($self, $method) = @_;
 
# Check if it's a lexical method
my $hinthash = (caller(0))[10];
if (exists $hinthash->{"Foo/$method"}) {
my ($package, $package_method)
= split /\//, $hinthash->{"Foo/$method"}, 2;
 
# Retrieve the subref and goto() it
my $subref = $package->can($package_method)
or croak("Lexically scoped Foo method $method points to a nonexistent method ${package}::$package_method");
return $subref;
}
elsif (my $super_method = $self->SUPER::can($method)) {
return $super_method;
}
else {
return undef;
}
}
 
1;
lexical-methods.pl
Perl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
use strict;
use warnings;
use 5.010;
 
use Foo;
 
my $obj = Foo->new;
 
# Look, we can call normal class methods:
$obj->foo;
 
# This will fail (which is why it's in an eval)
eval {
$obj->bar;
1;
} or do {
print "Unable to call 'bar' on Foo object in this lexical scope\n";
};
 
{
# Lexically allow method "bar" as a Foo method:
use Bar;
$obj->foo;
$obj->bar;
}
 
# Out here, normal method calls still work
$obj->foo;
 
# Kaboom!
$obj->bar;

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.