この記事は「はてなエンジニア Advent Calendar 2021」の12日目の記事です.
qiita.com
昨日は id:cohalz の「distrolessのnonrootイメージを使おう」でした.
cohalz.co
関数のエクスポート
Perlのパッケージには, "エクスポート"という概念があります. 例えば, 次のようなモジュールがあったとしましょう:
package MyPkg;
sub f1 { ... }
sub f2 { ... }
sub f3 { ... }
パッケージMyPkg
に定義されたf1
, f2
, f3
の関数をパッケージの外から呼び出すには, 通常次のように呼び出す必要があります:
use MyPkg;
MyPkg::f1();
MyPkg::f2();
MyPkg::f3();
いちいち MyPkg
を書くのは面倒, ということで用意されたのが Exporter
モジュールです.
metacpan.org
このモジュールを使うと, グローバル変数@EXPORT
で自動的にエクスポートする関数を, @EXPORT_OK
でエクスポート可能な関数 を, それぞれ定義することができます. 例えば, MyPkg
で Exporter
を使って, 次のように書き換えたとしましょう:
package MyPkg;
use Exporter 'import';
our @EXPORT = qw(f1 f2);
our @EXPORT_OK = qw(f3);
sub f1 { ... }
sub f2 { ... }
sub f3 { ... }
この場合, f1
, f2
の関数は自動的にエクスポートされるため, 次のようにして呼び出すことができます:
use MyPkg;
f1();
f2();
f3();
f3
を呼び出したい時は, 次のようにMyPkg
をuse
するときに, 「この関数を利用します」と指定しなければなりません:
use MyPkg qw(f3);
f3();
@EXPORTの問題点
@EXPORT
と @EXPORT_OK
, どう使い分けるべきでしょうか. 先程の例では, Exporter
モジュールを使って関数をエクスポートしているパッケージが, MyPkg
だけだったので, どちらでも問題ないように思えます. しかし, 複数のパッケージでExporter
モジュールを使い, @EXPORT
で関数をエクスポートしていると, どうなるでしょうか.
use MyPkg1;
use MyPkg2;
use MyPkg3;
exported_function();
パッと見で, 「exported_function
はどのパッケージで定義されているんだ...?」となりますよね. このため, Perl::Critiでも Perl::Critic::Policy::Modules::ProhibitAutomaticExportation というルールが用意されていて, @EXPORT
を使って関数をエクスポートしようとすると警告してくれるようにすることができます.
metacpan.org
@EXPORT を @EXPORT_OK に置き換える
これを自動的に置き換えるにはどうすればいいでしょうか. 今回はPerlの静的解析ツールであるところのPPIを使って解決しました:
metacpan.org
use strict;
use warnings;
my $pkg = 'Foo';
my $path = 'lib/Foo.pm';
my $exported_function_names = get_exported_function_names($pkg);
my @files_using_exported_function = split /\n/, `git grep --name-only --word-regexp 'use $pkg;' t/`;
replace_file($_, $exported_function_names) for @files_using_exported_function;
replace_export_ok($path);
sub get_exported_function_names {
my $pkg = shift;
no strict qw(refs);
return [ @{ $pkg . '::EXPORT' } ];
}
sub replace_export_ok {
my ($path) = @_;
my $document = PPI::Document->new($path);
my $export = $document->find(
sub { $_[1]->isa('PPI::Token') && $_[1]->content eq '@EXPORT' }
);
return undef unless $export;
$export->[0]->set_content('@EXPORT_OK');
$document->save($path);
}
sub replace_file {
my ($path, $exported_function_names) = @_;
my $document = PPI::Document->new($path);
my $used_exported_function_names = $document->find(
sub { $_[1]->isa('PPI::Token') && any { $_[1]->content eq $_ } $exported_function_names->@* }
);
return undef unless $used_exported_function_names;
my $unique_used_exported_function_names = [ uniq sort map { $_->content } $used_exported_function_names->@* ];
my $use_statement = $document->find(
sub { $_[1]->isa('PPI::Statement::Include') && $_[1]->module eq $pkg }
)->[0];
my $package_word = $use_statement->find(
sub { $_[1]->isa('PPI::Token::Word') && $_[1]->content eq $pkg }
)->[0];
$package_word->insert_after(
PPI::Token::Word->new(sprintf(" qw(%s)", join(' ', $unique_used_exported_function_names->@*)))
);
$document->save($path);
}
この例では, lib/Foo.pm
にあるパッケージFoo
にある@EXPORT
を@EXPORT_OK
に置き換えています. lib/Foo.pm
だけでなく, Gitのgit grep
を使って, 実際にFoo
を使っているファイルの中身も書き換えて, 置き換え後も正しくテストが通るようにしています.
実際にとあるPerlプロダクトで試してみましたが, 95%くらいはこの素朴なスクリプト(をベースにしたスクリプト)で一括置換することができました.
実は今回始めてPPIを使ったのですが, ドキュメントとにらめっこして1時間ちょっとくらいで素朴な置き換えスクリプトが実装できて便利でした(ので, おそらくもっと良い書き方はありそうです).
PPIを使うことで, こういった細かい気になりポイントをガッとリファクタリング出来ることがわかったので, これからも良いアイデアが浮かんだらやっていきたいと思いました.
明日の担当は...
id:Windymelt さんです. よろしくおねがいします.
blog.3qe.us