Skip to main content

Making Dynamically Required Package Names More Discoverable in Perl

·1236 words·6 mins·
perl perlimports
❤️ It's great to see you here! I'm currently available on evenings and weekends for consulting and freelance work. Let's chat about how I can help you achieve your goals.

I’ve been using perlimports a lot at $work. I’m generally quite happy with perlimports, but it can get confused by modules which are being dynamically used. Consider the following case, where we are using a function to create new objects.

We’ll be using Git::Helpers::CPAN to look up the Git repository for a CPAN module (or distribution).

 1#!/usr/bin/env perl
 2
 3use strict;
 4use warnings;
 5use feature qw( say signatures );
 6no warnings qw( experimental::signatures );
 7
 8use Git::Helpers::CPAN ();
 9
10sub object_factory ( $class, $name ) {
11    return $class->new( name => $name );
12}
13
14my $module = object_factory( 'Git::Helpers::CPAN', 'Open::This' );
15say $module->repository->{url};

The object_factory() function takes two arguments. The first is a class name. In order to keep things simple, the class name will always be Git::Helpers::CPAN. The second argument is the name of a CPAN module to look up. When we run the script, the output is:

$ perl factory.pl
https://github.com/oalders/open-this.git

We’ve now established that the script compiles and runs. Based on the output of the script, we can confirm Git::Helpers::CPAN is being used.

Let’s run perlimports on it. We will use the --no-preserve-unused flag, which means that perlimports should delete use statements for modules which appear to be unused. The -i flag indicates that we’d like to perform an in place edit.

$ perlimports --no-preserve-unused -i factory.pl

The result is:

@@ -5,7 +5,6 @@ use warnings;
 use feature qw( say signatures );
 no warnings qw(experimental::signatures);

-use Git::Helpers::CPAN ();

 my $module = object_factory( 'Git::Helpers::CPAN', 'Open::This' );
 say $module->repository->{url};

What happened?

perlimports didn’t find a use of Git::Helpers::CPAN, like Git::Helpers::CPAN->new or $Git::Helpers::CPAN::VERSION in the code, so it assumed that Git::Helpers::CPAN was not being used at all and helpfully removed the offending use statement. perlimports isn’t smart enough to know that $class will at some point contain Git::Helpers::CPAN, so it comes to the conclusion that the Git::Helpers::CPAN serves no purpose here.

In order to prevent this from happening, we can use a handy trick.

 1#!/usr/bin/env perl
 2
 3use strict;
 4use warnings;
 5use feature qw( say signatures );
 6no warnings qw( experimental::signatures );
 7
 8use Git::Helpers::CPAN ();
 9
10sub object_factory ( $class, $name ) {
11    return $class->new( name => $name );
12}
13
14my $module = object_factory( Git::Helpers::CPAN::, 'Open::This' );
15say $module->repository->{url};

Did you spot the change?

-my $module = object_factory( 'Git::Helpers::CPAN', 'Open::This' );
+my $module = object_factory( Git::Helpers::CPAN::, 'Open::This' );

Let’s run perlimports again. This time no lines are removed. The package name is now discoverable as far as perlimports is concerned. Problem solved.

The Explanation
#

Please take my explanation for what it is: a bit of hand waving. I haven’t looked at the underlying code and I actually don’t know where this behaviour is documented, but when perl sees a bareword suffixed by :: and a package by this name has already been required, perl will assume this is a fully qualified package name.

For example, this script, which uses the :: suffix once on line 12 and twice on line 15, compiles without errors:

 1#!/usr/bin/env perl
 2
 3use strict;
 4use warnings;
 5
 6use Git::Helpers::CPAN ();
 7use Open::This         ();
 8
 9my $one = Git::Helpers::CPAN->new( name => 'Open::This' );
10
11# Invoke Git::Helpers::CPAN with the :: suffix
12my $two = Git::Helpers::CPAN::->new( name => 'Open::This' );
13
14# Also pass Open::This:: as the value rather than the quoted 'Open::This'
15my $three = Git::Helpers::CPAN::->new( name => Open::This:: );

Let’s see what happens after we remove one line.

 use warnings;

 use Git::Helpers::CPAN ();
-use Open::This         ();
 1#!/usr/bin/env perl
 2
 3use strict;
 4use warnings;
 5
 6# This script will NOT compile
 7
 8use Git::Helpers::CPAN ();
 9
10my $one = Git::Helpers::CPAN->new( name => 'Open::This' );
11
12# Invoke Git::Helpers::CPAN with the :: suffix
13my $two = Git::Helpers::CPAN::->new( name => 'Open::This' );
14
15# Pass Open::This:: as value rather than the quoted 'Open::This'
16my $three = Git::Helpers::CPAN::->new( name => Open::This:: );

We now get the following compilation error:

Bareword “Open::This::” refers to nonexistent package

Since there’s no longer a use or require of Open::This, the instantiation of $three triggers the compilation error.

Open::This is indeed a package which does exist and is locally installed, but since we haven’t included it before this point, the script will exit with an error.

The script includes a use Git::Helpers::CPAN, so there are no compilation errors about the two uses of Git::Helpers::CPAN::->new().

The main takeaway here is that if you’re going to use a class name as a bareword with the :: suffix, you’ll need to use or require that class first.

Other Uses
#

Maybe there are other interesting ways to use this. How about Moose attribute definitions? Consider the following code:

 1package Local::Antler;
 2
 3use Moose;
 4
 5has some_date => (
 6    is      => 'ro',
 7    isa     => 'DateTime',
 8    lazy    => 1,
 9    default => sub { DateTime->now },
10);
11
12__PACKAGE__->meta->make_immutable;
131;
14
15package main;
16
17sub do_something {
18    my $a = Local::Antler->new;
19    print $a->some_date;
20}

This script compiles and runs without errors. Why?

The some_date() accessor is lazy and we haven’t tried to access it yet. That means that the anonymous subroutine (DateTime->now) which was provided as an arg to default never gets run and our script runs in blissful ignorance of the weak point in the logic. Hopefully we don’t try to run do_something() later on in our code. If we do, we’ll get the following compilation error:

Can’t locate object method “now” via package “DateTime” (perhaps you forgot to load “DateTime”?)

Let’s switch the isa to use a bareword with the :: suffix on line 7.

 1package Local::Antler;
 2
 3use Moose;
 4
 5has some_date => (
 6    is      => 'ro',
 7    isa     => DateTime::,
 8    lazy    => 1,
 9    default => sub { DateTime->now },
10);
11
12__PACKAGE__->meta->make_immutable;
131;
14
15package main;
16
17sub do_something {
18    my $a = Local::Antler->new;
19    print $a->some_date;
20}

If we try to run this script, we’ll now get the following compile-time error:

Bareword “DateTime::” refers to nonexistent package

We now have a safety check in place. In order to get this script to compile we need to add the missing use statement.

+use DateTime ();
+
 has some_date => (
     is      => 'ro',
     isa     => DateTime::,

That gives us the following working script:

 1package Local::Antler;
 2
 3use Moose;
 4
 5use DateTime ();
 6
 7has some_date => (
 8    is      => 'ro',
 9    isa     => DateTime::,
10    lazy    => 1,
11    default => sub { DateTime->now },
12);
13
14__PACKAGE__->meta->make_immutable;
151;
16
17package main;
18
19sub do_something {
20    my $a = Local::Antler->new;
21    print $a->some_date;
22}

This is one of the more useful cases I’ve come across for using the :: suffix.

Verbosity
#

'My::Module' takes up the same amount of characters as My::Module::, so this syntax doesn’t actually make your code any more verbose. Deciding whether it makes your code more or less readable is left as an exercise for the reader.

Nota bene
#

Please note that while this is a handy trick to have up your sleeve, it could confuse colleagues who are not familiar with this behaviour. If you do introduce it, you may first want to give people a quick primer on what’s going on here.

Supported Perl Versions
#

I don’t know when this functionality was introduced, but it works on a Perl v5.8.9. If you’d like to try it yourself, you can get the env up and running quickly via

$ docker run -it perldocker/perl-tester:5.8 /bin/bash

Have fun with it!


Related

Finding Unused Perl Variables
·342 words·2 mins
perl linting
Detective Work with perlimports
·476 words·3 mins
perl Programming perlimports
Improving prove with Preview Windows
·241 words·2 mins
perl Programming testing tab completion fzf prove fd bat