Optional use of modules

proof-of-conceptimport Sun 4 April 2021

Sometimes you want to optionally use a module in your code: if it's available, then load it, but if not, you can still proceed. I had to do this recently, and used one of the common approaches, but have been thinking it would be nice if this were easier to do.

Let's say you've a module that processes CPAN distributions, and generates a report with all sorts of statistics and analyses. Someone else has written a module that generates a code complexity metric. So first you write:

use Perl::Metrics qw/ code_complexity /;

After some CPAN Tester fails, you discover that function was only added in version 1.37, so then you write:

use Perl::Metrics 1.37 qw/ code_complexity /;

But then you discover that Perl::Metrics won't install on some platforms / versions of perl, so now you want to make it optional.

Here's the usual way that's done

BEGIN {
    eval {
        require Perl::Metrics;
        Perl::Metrics->VERSION(1.37);
        Perl::Metrics->import(qw/ code_complexity /);
    };
}

And then in your code, if you want to check whether that module was loaded:

if ($INC{'Perl/Metrics.pm'}) {
    $complexity = code_complexity($dist);
}

Or you could just wrap an eval around it.

When a module is loaded, the partial path is stored in %INC, so you have to know that Perl::Metrics becomes Perl/Metrics.pm.

I think this probably feels at least a bit arcane to most Perl programmers. So I wondered if we can make this easier.

Try the first

Here was my first thought on syntax:

use optionally Perl::Metrics 1.37 qw/ code_complexity /;

if (optionally::loaded('Perl::Metrics')) {
    $complexity = code_complexity($dist);
}

I whipped up a module optionally, which lets you write this:

use optionally 'Perl::Metrics', '1.37', qw/ code_complexity /;

if (optionally::loaded 'Perl::Metrics') {
    ...
}

This is better, but you've had to add various quotes and commas to the use line, because of how it's parsed.

Sub-optimal.

At first I thought this should be smart / helpful: giving you the helper function, and deleting the entry in %INC, etc. But you might not be the only code using the module:

We're optionally using Perl::Metrics, but Perl::Grokker is also using it, not optionally, and it doesn't care about the version. Messing around with the symbol table, or pretending it hadn't been loaded, could pull the rug from Perl::Grokker.

Try the second

I've never played with Keyword::Simple, but this seemed like a good opportunity. So I made another module optionally, which supports different syntax:

use optionally;
optionally use Perl::Metrics 1.37 code_complexity;

This parses the line, but to make things simple for me, you don't need to quote the names of things you're importing. Nice, but still having to write the rest of the line differently from normal. Sure I could improve the parsing, but this was a quick hack, remember.

Try the third

At this point I realised that with the leading optionally it's just syntactic sugar for running the rest of the line at compile time. So an even hackier approach was to take the rest of the line and eval it.

so now you can write this:

use optionally;
optionally use Perl::Metrics 1.37 qw/ code_complexity /;

All you need to do is put the word "optionally" at the start of the line. But you have to "use optionally" beforehand, which isn't great.

But this is a hack with all sorts of problems, so don't use it!

Why bother?

There would be at least three benefits to adding this capability:

  1. It makes it much easier for people to optionally include modules.
  2. It makes it much clearer what's going on, when people read the code.
  3. Easier for static analysis to identify optional dependencies, to declare in distribution metadata.

Thinking of the third point, maybe the syntax could be:

use -recommended Perl::Metrics 1.37 qw/ code_complexity /;

This is less user-friendly though. Optional dependencies could just be declared as recommended by default.

If I were going to release one of these to CPAN, it would be the first one, even though the end-user syntax is clumsy. But now you're adding another dependency on CPAN to handle an optional dependency.

Is it worth adding syntax to the language for this? That would give a cleaner syntax for users, and remove the additional dependency. This is not something that people want to do "all the time", or even "a lot", but maybe they'd do it more often if it were easier?

Given that people really program in Perl+CPAN, then we should improve the interface between Perl and CPAN, so I think overall it's probably just about worth adding.

comments powered by Disqus