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

Now that eventually I could make it to work, I have an actual programming question. Fact is, giving a peek into (cherished module) autobox's docs, I concocted up the following tiny module:

# -*- Perl -*- use strict; use warnings; use 5.010; package Array::Extract; use base qw/autobox/; sub import { my $class = shift; $class->SUPER::import(ARRAY => 'Array::Extract::Work'); } package Array::Extract::Work; sub extract { my $aref=shift; my @out=@{$aref}[@_]; @$aref=@{$aref}[grep !($_ ~~ @_) => 0..$#$aref]; @out; } 1; __END__

Now, it's not to boast about it, which I've probably been doing already, the last few posts: but it exposes a situation I've already met and always found unsatisfactory: namely, not only do I hardcode the Array::Extract::Work (which is nothing but a "helper" package) name, but I even do it twice, once as a bareword and once as a string. Needless to say, I find this very inelegant in the first place, and possibly error prone in a more complex application.

One solution that sprang to mind for some fractions of a second is to use another one of my favourite techniques: "code in @INC." But thinking of it better, it would really be to shoot... well, I can't remember the idiomatic English phrase, but I guess you understood what I mean!

Another solution that springs to mind it to refactor the module like thus:

# -*- Perl -*- use strict; use warnings; use 5.010; package Array::Extract; use base qw/autobox/; my $pkgname; { package Array::Extract::Work; $pkgname = __PACKAGE__; sub extract { my $aref=shift; my @out=@{$aref}[@_]; @$aref=@{$aref}[grep !($_ ~~ @_) => 0..$#$aref]; @out; } } sub import { my $class = shift; $class->SUPER::import(ARRAY => $pkgname); } 1; __END__

Indeed, this does fit the bill. But I'm concerned about other possible problems: i.e. in this case "pieces" of the respective packages are sparse all over the module's code. And I wonder how things could grow, in a more realistic and complex case...

What would people do, anyway? At least those that share my concerns for elegance, that is...

--
If you can't understand the incipit, then please check the IPB Campaign.

Replies are listed 'Best First'.
Re: How not to hardcode a package name?
by moritz (Cardinal) on Aug 28, 2008 at 09:24 UTC
    You can solve any problem in computer science by introducing another abstraction layer, or so they say.
    package Array::Extract; our $worker_pkg; ... package Array::Extract::Worker; BEGIN { $Array::Extract::worker_pkg = __PACKAGE__ } ...

    (Not tested).

    That works even for separate files, and can be overridden by other modules.

    Update: I choose BEGIN out of habit, maybe something later like CHECK or INIT might be better, but then again they might cause problems with mod_perl.

      package Array::Extract; our $worker_pkg; ... package Array::Extract::Worker; BEGIN { $Array::Extract::worker_pkg = __PACKAGE__ }

      I didn't know I could do that. Or better: I hadn't thought I could do that. Of course the nice point is that in the BEGIN block I don't need to specify the full package name of $worker_pkg, since our creates an alias to it in all the lexical scope surrounding it.

      Similarly, I may still be using a regular lexical variable. I'm wondering if there are any possible drawbacks/differences/gotchas with one approach wrt the other.

      Update: I choose BEGIN out of habit, maybe something later like CHECK or INIT might be better, but then again they might cause problems with mod_perl.

      I must admit I know next to nothing about those special blocks, and I should probably read up something. Re mod_perl, I'm not a web programmer, but it's good to know anyway. Although I can't understand why: if it's valid Perl, it should continue to work in that environment too...

      --
      If you can't understand the incipit, then please check the IPB Campaign.
        Similarly, I may still be using a regular lexical variable. I'm wondering if there are any possible drawbacks/differences/gotchas with one approach wrt the other.

        Lexical (my) variable: good encapsulation trough limited scope, but you're forced to keep those two packages in the same file. You can't override it from the outside, which can be both good and bad, depending on whether you favor security or extensibility.

        For package variables it's just the other way round ;-)

        Corion pointed out that this approach does "action at a distance", which you probably can't entirely avoid unless you do symbol table hackery. my variables limit the scope of the spooky action, though.

        I must admit I know next to nothing about those special blocks, and I should probably read up something. Re mod_perl, I'm not a web programmer, but it's good to know anyway. Although I can't understand why: if it's valid Perl, it should continue to work in that environment too...

        The various compilation and execution phases are a not-so-simple (but really powerful) beast. CHECK and INIT are not executed inside a string eval, which is documented in perlmod, and apparently (I'm not a mod_perl user either) what mod_perl does to execute its scripts and modules.

        I don't know if that's a limitation in the implementation or a design goal, though.

      Maybe yet another layer of abstraction might make it even nicer:

      package Array::Extract; my $worker_pkg; my $priority = -1; sub import { my ($class, %args) = @_; if (exists $args{priority}) { # Register new implementation if ($args{priority} > $priority) { $worker_pkg = caller; $priority = $args{priority}; } } else { warn "Using $worker_pkg, priority $priority\n"; $class->SUPER::import(ARRAY => $worker_pkg); } } # Worker.pm package Array::Extract::Worker; use Array::Extract priority => 1; # somewhere else use Array::Extract::Worker; # has to be used first use Array::Extract;

      This avoids these global variables that have been described as "action at a distance". Using "priority", you can even have several implementations, and it will choose the one with the highest priority; but that's just an extra bonus, it'll work without priorities too.

Re: How not to hardcode a package name?
by Corion (Patriarch) on Aug 28, 2008 at 09:19 UTC

    A little convention can go a long way:

    $class->SUPER::import(ARRAY => __PACKAGE__.'::Work');

    If you want to be more fancy/flexible, you could do it like this:

    package Array::Extract; my $worker_package = __PACKAGE__ . '::Work'; # or whatever other name +you want sub worker_extract { ... your code ... }; { no strict 'refs'; *{"$worker_package\::extract"} = \&worker_extract; };
      A little convention can go a long way:

      $class->SUPER::import(ARRAY => __PACKAGE__.'::Work');

      I personally believe that it would still amount to "double hardcoding," albeit in a more "hyerarchical" (for what that it may mean in Perl 5) and thus clean way: what if I decide to change say "::Work" to "::Tool" later?

      If you want to be more fancy/flexible, you could do it like this:

      Well, this is just as ugly as working with symrefs generally is ;) and somehow unsatisfactory for leaving the same sub in two namespaces. (I presume that one could go just as far as deleting it from Array::Extract, but that would be kind of an exaggeration, I admit.) Or else I may actually adopt an anonymous sub to start with:

      package Array::Extract; my $worker_package = __PACKAGE__ . '::Work'; # or whatever other name +you want { no strict 'refs'; *{"$worker_package\::extract"} = sub { ... your code ... }; }

      And that would be nearly the same thing in most cases: that is, unless that sub creates a closure, which after all may be what I want anyway...

      The only problem I see with this approach is that instead of having a single sub like that, I may have quite a lot of them. (In which case I would probably put them into a hash and generate their package names programmatically...)

      --
      If you can't understand the incipit, then please check the IPB Campaign.
        sub worker { my ($self, $subclass, $name, $code) = (undef,'Worker','work'); if( @_ == 2) { ($self,$code) = @_; } elsif( @_ == 3) { ($self,$name,$code) = @_; } elsif( @_ == 4 ) { ($self,$subclass,$name,$code) = @_; }; my $n = "$self\::$subclass\::$name"; no strict 'refs'; *{$n} = $code; }; __PACKAGE__->worker('Tool' => 'toolit' => sub { ... }); __PACKAGE__->worker(sub { ... });
Re: How not to hardcode a package name?
by JavaFan (Canon) on Aug 28, 2008 at 10:09 UTC
    Personally, I don't have much problems with "hardcoded" package name, but, looking at your code fragment, I wonder, "why have another class anyway?". Wouldn't the code just work if you remove the line "package Array::Extract::Work;" and use
    $class->SUPER::import(ARRAY => __PACKAGE__);

      As I wrote, one reason is that I my code is modeled on a fragment from autobox's docs. But then as I also wrote, this is just one possible situation in which a similar case of (what I call) "double hardcoding" may happen. More seriously, the additional class is indeed a worker class, and is just there to keep things separated and clean: in a more realistic example, Array::Extract may have its own subs and methods, and I rightly want to define separately the methods that will work on autoboxed objects.

      --
      If you can't understand the incipit, then please check the IPB Campaign.
        True, but realize that when your codebase get larger, you may want to put the 'Array::Extract::Work' code in its own file, and you'll have the package name "hardcoded" in four different places: 1) the pathname of the file containing the Array::Extract::Work package; 2) the package statement in said file; 3) the use statement that tells perl to compile the code in the file; 4) the reference in the import routine.

        Be careful to not pick a solution that later on makes it harder to separate out the Array::Extract::Work to its own file.

Re: How not to hardcode a package name?
by JadeNB (Chaplain) on Aug 28, 2008 at 20:00 UTC
    I can't count the number of times that this question has bugged me in my own coding! It seems that there should be some CPAN module that does this, analogous to UNIVERSAL::require, which lets you require a module whose name is in a variable.

    While I know that it wouldn't be elegant, isn't there an eval-based way to do this? I tried the simple-minded
    my $a = 'Package'; eval "package $a";,
    but it doesn't work (I suspect because I don't really understand the eval EXPR form).

      The package of a line of code is set when that line is compiled.

      Furthermore, package is lexically scoped, so you can't just put the eval in a BEGIN.

      You can do

      my $p = 'Package'; my $c = 'print(__PACKAGE__);'; # Some code eval "package $a; $c";
        Thanks! Does this:
        The package of a line of code is set when that line is compiled.

        Furthermore, package is lexically scoped, so you can't just put the eval in a BEGIN.

        mean that it can't be done, or just that an eval is not the way to do it? (If I understand your code snippet correctly, you're saying that I can build up all the code I want in that package into a single string and eval that—but I'm sure that's not Best Practice, to say the least.)