本連載では第一線の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
によるインポート/
このとき、ExModule
上で何が起きているかを見ていきます。Exporter
のimport
メソッドを最小限にしたコードを次に示します。
呼び出し側が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
PPI
の使い方について詳しくは、本誌Vol.
関数の呼び出し方から特徴を分析
PPI
で解析するには、解析対象の特徴を分析することが重要です。呼び出された関数を解析したいので、関数の呼び出し方の特徴を考えていきます。
Perlでは、パッケージを指定して関数を呼び出す方法はさまざまありますが、ここでは一般的な呼び出し方として次の2通りを扱います。
- パターン1:
ModuleA::func
- シンボルテーブルから関数を呼び出す
- パターン2:
ModuleB->method
ModuleB::method("ModuleB")
と等価
パターン1と2から、呼び出される関数は[モジュール名][:: or ->][関数名]
となっていることがわかります。
PPIによる静的解析で自動インポートを実現
関数の呼び出し方の特徴を考えたので、この特徴を利用して関数が呼び出されている箇所を解析し、必要なモジュールを抽出して、インポートしていきます。
呼び出された関数を解析する
PPI
で関数の呼び出し方をどのように解析すればよいかを知るために、関数の呼び出し方で挙げたパターン1と2をPDOMに変換します。
結果から、単語を表すPPI::Token::Word
を解析すると、インポートが必要なモジュールを抽出できることがわかります。
インポートが必要なモジュールを抽出する
まずは、パターン1のモジュールと関数を抽出します。モジュール名の先頭は大文字で始まるというPerlの規則を利用して、正規表現でモジュールと関数を抽出します。
次に、パターン2のモジュールと関数を抽出します。PPI::Token::Word
のmethod_
メソッドを利用して、パターン2であることを判定できます。sprevious_
で空白など無用なものを除いて、メソッド名の前にあるモジュールを取得します。
以上から、パッケージを指定して関数が呼ばれている箇所を解析し、インポートが必要なモジュールを抽出できました。
抽出したモジュールをインポートする
対象となるファイル上で、インポート済みではないモジュールだけインポートする必要があります。PPI
を利用すると、use
文やrequire
文を表すPPI::Statement::Include
によって、すでにインポートされたモジュールの一覧を取得できます。
インポート済みのモジュールとインポートされていないモジュールの差分をArray::Diff
モジュールで計算すると、インポートされていないモジュールに限定できます。
次に、対象となるファイルにインポートします。インポートするモジュールのuse
文をPPI
を利用して先頭に挿入することで、必要なモジュールをインポートします。
以上から、対象となるファイルに必要なモジュールをインポートした内容を得られました。自動インポート後の結果を利用してもとのファイルを置換すると、use
文の漏れを防ぐことができます。