本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはmomochiさんで、テーマは
本稿のサンプルコードは、執筆時点
自動インポートで定型作業の効率化
Perlでは、モジュールを利用するにはuse文を追加し、モジュールが不要になった場合はuse文を削除するのが定型作業としてあります。コードベースが複雑になるほど、人間がuse文を過不足なく管理するにはコストを要します。そこで、自動インポートによって過不足なくuse文を管理することで開発効率を向上できます。
たとえば、Go言語ではgoimportsを使うと自動で必要なモジュールをインポートするので、効率的に開発できます。Perlは自由な書き方ができるので、同様の自動インポートをPerlで実現するのは難しいと思われるかもしれませんが、実はPerlでも同様の機能を実現できます。
本稿では、
Perlでインポート/エクスポートするしくみ 
自動インポートを実装するには、インポート/Exporterモジュールのしくみについて知る必要があります。
シンボルテーブルと型グロブ
Exporterのしくみを理解するために、まずはシンボルテーブルと型グロブについて解説します。
シンボルテーブルとは、変数自体を格納するグローバルアクセス可能なハッシュです。シンボルテーブルはキーと値のペアによって、変数名からその値への対応を表現します。たとえばFooパッケージ上で、our $iのようにパッケージ変数を宣言すると、パッケージに紐付いたFoo::iがシンボルテーブルに登録されます。
Perlでは同一の変数名でも$、@といったシジルと呼ばれる添え字によって変数のデータ型が異なります。そのため、シンボルテーブルには同名でもそれぞれのデータ型への対応を持つ必要があります。Perlでは、*symとすることでsymという名の変数のすべてのデータ型を表現できます。これは型グロブと呼ばれます。したがって、シンボルテーブルは変数名をキー、型グロブを値として持ちます。
型グロブに対する代入はエイリアス操作を行います。たとえば*sym2 = *sym1とすると、sym1のすべてのデータ型にsym2でアクセス可能となります。特定のデータ型のみをエイリアスするには、*sym2 = \$sym1のようにリファレンスを代入します。
Exporterによるインポート/エクスポート 
Exporterの一般的な使い方を示し、Exporterによるインポート/
package ExModule;
use Exporter 'import';
# エクスポートしたいシンボルを定義
our @EXPORT = qw(foo);
sub foo { print 'Hello World'; }
use ExModule qw(foo);
foo();
このとき、ExModule上で何が起きているかを見ていきます。Exporterのimportメソッドを最小限にしたコードを次に示します。
package Exporter;
sub import {
  my $pkg = shift;
  # 呼び出したパッケージ名
  my $callpkg = caller(0);
  # Exporterのimportを使うかどうか                      ¬
  if ($pkg eq "Exporter" && @_ && $_[0] eq "import") {   |
    # importをエイリアス                                 |
    *{$callpkg."::import"} = \&import;                   |❶
    return;                                              |
  }                                                      」
  # ExModuleでour宣言された@EXPORT変数                  ¬
  my $exports = \@{"$pkg\::EXPORT"};                     |
  # 呼び出し側の関数としてエイリアス                     |
  for (@$expors} {                                       |❷
    *{"$callpkg\::$_"} = \&{"$pkg\::$_"};                |
  }                                                      」
}
呼び出し側がExModuleをインポートし、foo()を実行するときの全体の処理を解説します。
はじめに、ExModuleでExporter->import('import')が実行されます。これは、Exporterで定義されたimportメソッドを使うという意味になります。❶が実行され、型グロブに対して代入することでExporterのimportメソッドをエイリアスします。
次に、呼び出し側でExModule->import('foo')が実行されます。❷が実行され、ExModuleで定義した@EXPORT変数の値を取得し、呼び出し側の関数としてfooをエイリアスします。
最後に、呼び出し側でfoo()が実行されます。Perlでは名前解決する際にmy宣言やour宣言された変数がなければパッケージ変数であると仮定して探索します。したがって、呼び出し側のシンボルテーブル内でfooが探索され、これはエイリアス済みなので名前解決されます。
以上のとおり、Exporterはシンボルテーブルと型グロブを巧みに利用することで、インポート/
静的解析による自動インポート
Perlでは、パッケージ名を指定して関数を呼び出すことができます。しかし、指定されたパッケージ名がインポートされていないとエラーとなります。したがって、パッケージ名を取得して必要なモジュールを自動でインポートできれば、モジュールがインポート済みかどうかを確認する必要がなくなって楽になります。
本節では、静的解析器として代表的なPPIモジュールを利用して呼び出された関数を解析し、その関数が定義されているモジュールをインポートする方法を解説します。
静的解析器PPIモジュール
PPIは、Perlのソースコードを入力として、内部でソースコードを意味のある単位で分けたPDOM
use Test;
PPI::Document
  PPI::Statement::Include
    PPI::Token::Word 'use'
    PPI::Token::Whitespace ' '
    PPI::Token::Word 'Test'
    PPI::Token::Structure ';'
  PPI::Token::Whitespace '\n'
PPIの使い方について詳しくは、本誌Vol.
関数の呼び出し方から特徴を分析
PPIで解析するには、解析対象の特徴を分析することが重要です。呼び出された関数を解析したいので、関数の呼び出し方の特徴を考えていきます。
Perlでは、パッケージを指定して関数を呼び出す方法はさまざまありますが、ここでは一般的な呼び出し方として次の2通りを扱います。
- パターン1:ModuleA::func
- シンボルテーブルから関数を呼び出す
- パターン2:ModuleB->method
- ModuleB::method("ModuleB")と等価
パターン1と2から、呼び出される関数は[モジュール名][:: or ->][関数名]となっていることがわかります。
PPIによる静的解析で自動インポートを実現
関数の呼び出し方の特徴を考えたので、この特徴を利用して関数が呼び出されている箇所を解析し、必要なモジュールを抽出して、インポートしていきます。
呼び出された関数を解析する
PPIで関数の呼び出し方をどのように解析すればよいかを知るために、関数の呼び出し方で挙げたパターン1と2をPDOMに変換します。
my $source = <<'EOS';
ModuleA::func;
ModuleB->method;
EOS
my $doc = PPI::Document->new(\$source);
# PDOMを木構造でダンプする
my $dumper = PPI::Dumper->new($doc);
$dumper->print;
# パターン1:ModuleA::func
PPI::Token::Word 'ModuleA::func'
# パターン2:ModuleB->method
PPI::Token::Word 'ModuleB'
PPI::Token::Operator '->'
PPI::Token::Word 'method'
結果から、単語を表すPPI::Token::Wordを解析すると、インポートが必要なモジュールを抽出できることがわかります。
インポートが必要なモジュールを抽出する
まずは、パターン1のモジュールと関数を抽出します。モジュール名の先頭は大文字で始まるというPerlの規則を利用して、正規表現でモジュールと関数を抽出します。
# $wordはPPI::Token::Wordである
my ($module, $func) = $word =~ /((?:[A-Z]\w*::)+)(\w+)/;
# 末尾に::が付いているので削除
$module = substr($module, 0, -2);
次に、パターン2のモジュールと関数を抽出します。PPI::Token::Wordのmethod_メソッドを利用して、パターン2であることを判定できます。sprevious_で空白など無用なものを除いて、メソッド名の前にあるモジュールを取得します。
my $prev = $word->sprevious_sibling;
if($word->method_call && $prev eq '->') {
  my $module = $prev->sprevious_sibling;
  my $func = $word;
}
以上から、パッケージを指定して関数が呼ばれている箇所を解析し、インポートが必要なモジュールを抽出できました。
抽出したモジュールをインポートする
対象となるファイル上で、インポート済みではないモジュールだけインポートする必要があります。PPIを利用すると、use文やrequire文を表すPPI::Statement::Includeによって、すでにインポートされたモジュールの一覧を取得できます。
インポート済みのモジュールとインポートされていないモジュールの差分をArray::Diffモジュールで計算すると、インポートされていないモジュールに限定できます。
# $filenameは対象となるファイル名
my $doc = PPI::Document->new($filename);
my $incs = $doc->find('PPI::Statement::Include');
my $inc_modules = [ map { $_->module } @$incs ];
# $maybe_need_modulesは抽出したモジュール
my $need_modules = Array::Diff->diff(
  $inc_modules, $maybe_need_modules
)->added;
次に、対象となるファイルにインポートします。インポートするモジュールのuse文をPPIを利用して先頭に挿入することで、必要なモジュールをインポートします。
# PPI::Statement::Includeに変換する
my $create_inc = sub {
my $str = shift;
my $d = PPI::Document->new(\"$str");
$d->find_first('PPI::Statement::Include')->clone;
};
for my $module (@$need_modules) {
my $inc = $create_inc->("use $module;");
$doc->first_element->insert_before($inc);
}
# もとのファイルの内容にuse文が追加された状態
my $formatted = $doc->serialize;
以上から、対象となるファイルに必要なモジュールをインポートした内容を得られました。自動インポート後の結果を利用してもとのファイルを置換すると、use文の漏れを防ぐことができます。


