✨ This post was last updated on July 12, 2024
"The Microscope Book" by Orin Zebest is licensed under CC BY-SA 2.0 .
The Magic of Dynamically Required Modules#
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 required. 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.
The Problem#
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
Addendum the First: The Documentation#
After I published this article, there was a discussion on
Mastodon about what sort
of documentation exists for this feature. I asked on #toolchain on irc.perl.org
where this might be documented. Matthew Horsfall kindly pointed out that this
is mentioned in perlobj
. It comes up in Invoking Class
Methods with the
relevant docs reading:
Because Perl allows you to use barewords for package names and subroutine names, it sometimes interprets a bareword’s meaning incorrectly. For example, the construct
Class->new()
can be interpreted as either'Class'->new()
orClass()->new()
. In English, that second interpretation reads as “call a subroutine namedClass()
, then callnew()
as a method on the return value ofClass()
”. If there is a subroutine namedClass()
in the current namespace, Perl will always interpretClass->new()
as the second alternative: a call tonew()
on the object returned by a call toClass()
.You can force Perl to use the first interpretation (i.e. as a method call on the class named “Class”) in two ways. First, you can append a :: to the class name:
Class::->new()
Perl will always interpret this as a method call.
There is a further nod in the section onIndirect
Object Status. I
encourage you to consult perldoc
if you’re curious to learn more.
Addendum the Second: ::
as a Prefix#
In the aforementioned discussion on Mastodon, GARU shared this fun tip:
I often use something similar with Data::Printer, by writing “::p $var” instead of “p $var”, then running the code with -MDDP. This way, once I’m done debugging, if I forget any p() calls inside the code it will die spectacularly at compile time ❤️
Probably there’s another blog post just in this little tidbit, but I don’t have the real estate for this today. It’s a nice trick to keep in mind.
Have fun with it!