本連載では第一線の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
文の漏れを防ぐことができます。