これはPerlアドベントカレンダー2021の12日目の記事です。
昨日はshogo82148さんで、Perl 5.35.5 の iterating over multiple values at a time を先取りでした。
今回はPerlでもいい感じのモジュールのImportをしてくれるApp::perlimportsの紹介です。
App::perlImports
このモジュールはThe Perl and Raku Conference 2021で作者本人からトークされていて、スライド及び動画が上がっています。
スライドはGitHub上にHTMLがあるので手で落とす必要があるようです...。(GitHubPagesは特に使っていない模様)
curl https://raw.githubusercontent.com/oalders/presentations/main/slides/6-perlimports/remark.html -o perlimports.html && open perlimports.html
「Where Did That Symbol Come From?」とトークタイトルが示す通り、Perlのシンボルがどこからimportされたかを明確にしたいというのが実装の動機になっているようです。合わせて動き自体はgolangでimportしているモジュールをいい感じに管理してくれるgoimportsを参考にしているようです。
このモジュールについて説明するにはまず、Perlのモジュールロードがどのように行われているかを確認する必要があります。ざっと見てみましょう。
Perlのモジュールロード
Perlはモジュールをロードする方法に主にuse
を使った方法とrequire
を使った方法があります。*1
歴史的経緯でrequire
の方が先に出ていて、 Perl5でuse
が導入されています。
requireはモジュールロード以外に外部ファイルの読み込みにも使われます。
近年はuse
を使ってモジュールロードをするのが一般的です。
例えば、PerlにデフォルトでバンドルされているHTTPクライアントのHTTP::Tiny
を使う場合は次のようにロードします。
use HTTP::Tiny; my $ua = HTTP::Tiny->new();
ロードタイミング
Perlは動的言語でありますが、use
を使ったモジュールロードの処理は、該当のPerlコードをPerlインタプリタが読み込む、コンパイルタイムに行われます。
例えば存在しないHoge
モジュールをロードしようとすると、次のようにprintが走ることはなくエラーで死にます。
#!/usr/bin/env perl use strict; use warnings; print "hello"; use Hoge;
❯ perl hoge.pl Can't locate Hoge.pm in @INC (you may need to install the Hoge module) (@INC contains: /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/site_perl/5.32.0/darwin-2level /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/site_perl/5.32.0 /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/darwin-2level /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0) at hoge.pl line 2. BEGIN failed--compilation aborted at hoge.pl line 2.
対して、各関数呼び出しなどの処理は実行時に解決されます。 そのためuseを忘れても、対象のメソッドを実行しない限りは処理が止まることはありません。 例えば次のコードは、HTTP::Tinyをuseしていませんが、呼び出す前にdie(Perlでのエラー終了)をして処理を止めるので、ロードエラーは発生しません。
#!/usr/bin/env perl use strict; use warnings; print "hello\n"; die; my $ua = HTTP::Tiny->new(); $ua->get('https://example.com');
perl hoge.pl
hello
Died at hoge.pl line 6.
useの処理
ではuseは何をしているのでしょうか。Perldoc.jpを見るとこう書かれています。
指定したモジュールから、現在のパッケージにさまざまな内容をインポートします; 多くは、パッケージのサブルーチン名や、変数名に別名を付けることで、 実現されています。 これは、以下は等価ですが
BEGIN { require Module; Module->import( LIST ); }
BEGIN { }
はBEGIN ブロックと呼ばれるもので*2、コンパイルタイムに実行させるという処理です。
中ではrequire
を使い、モジュールを読み込んだ後に、そのモジュールにあるimport
メソッドを実行しています。
つまり、前述したPerlのモジュールロードのuse
とrequire
を比較すると次のような違いがあることがわかります。
項目 | use | require |
---|---|---|
ロードタイミング | コンパイルタイム | 実行時 |
副作用 | importを実行する | 読み込みのみ |
整理したところで、このimport
とは一体何でしょうか。
一般的には、これはモジュールで定義しているメソッドを、呼び出し元のコードで定義したかのように使うために実行されるものです。
詳しく見てみましょう。
import
Perlでは特定のモジュールをuse
すると使えるようになるメソッドがいくつか存在します。
例えばコアモジュールのJSONを扱うライブラリであるJSON::PPをuseすると、任意のPerlのハッシュをJSON文字列化できるencode_json
が使えるようになります。
use strict; use warnings; use JSON::PP; print encode_json({hoge => 'hello', isBool => \0});
encode_json
はJSON::PPで定義されているメソッドなので、通常はJSON::PP->encode_json
のように使う必要があります。
import
は特定のメソッドを、use
したタイミングで通常の関数のように呼び出せるようによしなにやってくれる処理になっています。
この処理のことをPerlでは関数のエクスポートなどと読んでいます。
近年ではExporterモジュールが行っています。
あるモジュールで、Exporterモジュールを継承、もしくはuse
すると、そのモジュール内で自動的に関数のエクスポート処理をimport
関数として登録してくれます。
あくまでExporter
を使った場合なので、独自にimport
関数を定義することも可能です。
その場合は通常の関数のように呼び出せるようによしなにやってくれる処理
以外の処理をuse
時に行うように差し込むことが可能となります。
例えばClass::Accessor::Liteはimport
をExport以外の目的に使っており、オブジェクトのアクセサを生成する処理を行っています。
use Class::Accessor::Lite ( new => 1, rw => [ qw(foo bar) ], ro => [ qw(baz) ], wo => [ qw(hoge) ], );
Perlでモジュールをuse
する際に、exportしたい関数名の指定ではないような引数を渡しているモジュールは、ほぼ独自にimport
を書いていると考えても良いでしょう。
use時に差し込まれるモノたち
Exporterを使っているモジュールがuse
されたときに差し込むものは、モジュール側で定義されている@EXPORT
配列に積んだシンボルになっています。((ここでのシンボルはPerlのメソッド(関数、サブルーチン)の他に、スカラ変数や配列も含みますが、Exporter
を使う上ではメソッド以外のシンボルを使うと問題が発生しがちなので、基本はメソッドのことです
))
例えば、Hoge::First
モジュールをuseしたときに、hogeメソッドを差し込みたい場合は、次の様に定義します。
package Hoge::First; use strict; use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/); #サブルーチンの名前を文字列で配列の中にいれるとEXPORTされる sub hoge { say 'this is first!'; } 1;
Perlは@EXPORT
に積まれているオブジェクトをすべてロードします。
このためuse
しているモジュールが、内部で@EXPORT
に何かしら詰めていた場合、予期せぬシンボルをロードする可能性があります。
また、あるPerlファイルの中で出てきているメソッドが、一体どのモジュールから提供されているか、はたまたデフォルトのメソッドであるかの判断が難しくなってしまいます。
使用しているメソッドが、どのモジュールから提供されているのが解りづらいのは、リファクタリングの結果すでにあるモジュールが提供しているメソッドを使わなくなったのに、use
し続けてしまうモジュールが出たり、逆にプログラミングしている上で使えるだろうと思って書いたメソッドが、実はuse
しないといけなかったなどの問題を誘発しがちです。
また、同じ名前のオブジェクトをロードしてしまうと、呼び出し順序によって使われる関数が異なってしまいます。
例えばHoge::First
とHoge::Second
モジュールでそれぞれhoge
をEXPORTしてみます。
package Hoge::First; use strict; use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/); sub hoge { say 'this is first!'; } 1;
package Hoge::Second; use strict; use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/); sub hoge { say 'this is second!'; } 1;
#!/usr/bin/env perl # main.pl use strict; use warnings; use Hoge::First; use Hoge::Second; hoge();
実行すると、最後にロードされたSecondが出てきます。
❯ perl -Ilib hoge.pl this is second!
@EXPORT
はこのような思いがけない処理が走る可能性があるので、Exporterモジュールのコメントにも「とりあえずEXPORTするのはやめよう」といったことが書かれています。
pointoht.ti-da.net
さらにはPerlのLinterであるPerl::Criticにも@EXPORT
を警告するルールが追加されています。
とはいえ関数のエクスポートは便利なので、それはそれで使用したいです。
Perlでは@EXPORT
の他に@EXPORT_OK
があります。これはデフォルトではすべてをロードせず、use
時にエクスポートしたい関数名を書くことを強制させる機能です。
例えばこんな感じにuse
時に関数名を指定します。
use JSON qw(encode_json);
実は@EXPORT
で宣言している場合もuse
時に関数名を指定している場合はそのメソッドしかエクスポートされません。関数名を何も指定せずに()
を書いた場合は何もエクスポートされないため、宣言的に関数名を書くようにするとほぼ@EXPORT_OK
と同様の振る舞いをします。
自分が書いているコードに出てくるメソッドの宣言元をはっきりさせる単純な解決方法はEXPORTをやめ、Hoge::Foo->bar()
の様にフルパスでメソッドを実行することですが、テストヘルパなどEXPORTした方が利便性が高いものもあるでしょう。
decode_utf8
など、EXPORTされたメソッドを使うのがイディオム的に認知されているものもあります。
エクスポートした関数を使うのと、どこから取り込んできたのかの視認性を高めるには、use
時にエクスポートしたい関数名を漏れなく書くしか無いでしょう。
しかしこれを人間がやるのは面倒!!! ということで作られたのがperlimportsです。
....と、ここまで書いたところで奇跡的に裏番組のはてなエンジニア Advent Calendar 2021でid:papixさんが紹介している内容と前提知識が同じだった!!そんなことってあるんだ
あらためてApp::perlImports
- どこから来たのかわからないシンボルをエクスポートしている
use
を特定したい @EXPORT
を全部インポートするのではなく、一部だけ指定してインポートしたい
これらの課題に対して、golangのgoimportsを参考に、構文解析ライブラリPPIの力を借りて誕生したのがApp::perlimportsです。 metacpan.org
著者のOALDERSさんはLWPなどのメンテナもやられています。
使ってみる
例えば次の様なモジュールがあったとしましょう。
package MyPkg::One; use strict; use warnings; use Exporter 'import'; our @EXPORT = qw(f1); sub f1 { print "f1\n"; } 1;
package MyPkg::Two; use strict; use warnings; use Exporter 'import'; our @EXPORT_OK = qw(f2); sub f2 { print "f2\n"; } 1;
package MyPkg::Three; use strict; use warnings; use Exporter 'import'; our @EXPORT_OK = qw(f3); sub f3 { print "f3\n"; } 1;
package MyPkg::Four; use strict; use warnings; use Exporter 'import'; our @EXPORT = qw(f4); sub f4 { print "f4\n"; } 1;
これをhoge.pl
から以下の様に呼び出します。
use strict; use warnings; use MyPkg::One; use MyPkg::Two; use MyPkg::Three; use MyPkg::Four; f1(); f2(); MyPkg::Three->f3();
hoge.pl
の状況は次のとおりです。
f1
はMyPkg::OneがEXPORTしているものを使っているf2
はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れているf3
はフルパスで呼び出しているf4
はuseしているが特に使っていない
このコードはそのまま実行すると, f2の解釈で実行時エラーが発生します。また、どのモジュールからエクスポートされているのかパット見ではわかりません。
さらにMyPkg::Fourは必要がないf4
をエクスポートしてしまっています。
❯ perl -Ilib hoge.pl f1 Undefined subroutine &main::f2 called at hoge.pl line 10.
これをApp::perlimportsに通すと、以下のようにコードが書き換わります。
❯ perlimports --libs lib hoge.pl use strict; use warnings; use MyPkg::One qw( f1 ); use MyPkg::Two qw( f2 ); use MyPkg::Three (); use MyPkg::Four (); f1(); f2(); MyPkg::Three->f3();
見ると次の様な変換が行われています。
f1
,f2
がEXPORT元のモジュールが明記されるようになった- エクスポートした関数を使用していない
Three
とFour
は、空配列を指定することになり、何もエクスポートしなくなっている
変換されたコードはそのまま動かす事ができます。(今回はパイプで動かしています) こうすると、実行時エラーを未然に防ぐことが可能になり、どのモジュールからエクスポートしているか、パット見でわかるようになりました。
❯ perlimports --libs lib hoge.pl | perl -Ilib
f1
f2
f3
さらに、無駄にロードしているMyPkg::Four
を削除することも可能です。
❯ perlimports --libs lib --no-preserve-unused hoge.pl
use strict;
use warnings;
use MyPkg::One qw( f1 );
use MyPkg::Two qw( f2 );
use MyPkg::Three ();
f1();
f2();
MyPkg::Three->f3();
こうすると、無駄なファイルをuse
し続ける事もなくなり便利ですね!
インストール方法
cpanm
もしくはcpm
を使ってインストールしましょう
$ cpanm App::perlimports
$ cpm install -g App::perlimports
$ plenv rehash
使い方
CLIツールとして提供されているので、perltidyの様にコマンドで実行します。
$ perlimports --libs lib,local/lib/perl5 -f hoge.pl
主要なオプションを見てみます。
--libs
-f
- 変換対象のファイル名
--no-padding
- perlimportsが変換したあと、モジュール名の前後に余計な空白をいれない
- デフォルトでは
use Foo qw( bar baz );
の様になるが、--no-padding
を指定するとuse Foo qw(bar baz);
になる
--inplace-edit
,-i
- デフォルトでは変換したコードは標準出力に出るが、このオプションを指定すると変換元のファイルを上書きする
--no-preserve-unused
- 使っていなさそうなモジュールをuseしないようにする
注意点
自動でuseしてくれる訳ではない
下手にgoimportsを使ったことがあると誤解しやすい点で、perlimportsの(現状)の興味の対象はEXPORTしたシンボルをいい感じに特定することのみです。goimportsはimportの追加などもしていましたが、それに対応する機能はありません。モジュールのuse
し忘れなどは興味の範囲外になっています。
例えばMyPkg::One->f1()
の様なメソッド呼び出しをしていて、MyPkg::One
をuseし忘れた場合、perlimportsはこれを補完しません。
また、そもそもuse
しているモジュールがない状態で実行しても補完はしてくれません。
そのためモジュールのuse忘れなどのimportエラーは別系統で判断する必要があります。
❯ cat hoge.pl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; f1(); ~/workspace/test3 via 🐪 v5.34.0 ❯ perlimports --libs lib -f hoge.pl use strict; use warnings; use FindBin (); use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; f1();
関数エクスポート以外のuseは工夫の余地がある
また、Class::Accessor::Lite
やClass::Enumemonのようなuse
時にエクスポートする関数名を指定する以外に、独自にimports
を定義しているモジュールは、perlimportsはuse
したものを使っていないと見なしてしまいます。これはperiimportsの作者によって名指しでperlimportsの自動整形下に置かない様に書かれているモジュール群か、構文解析の結果自動調整を逃れたもの以外は、空配列のimportに変換されてしまいます。
❯ cat hoge.pl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite ( rw => [qw/ hoge/], ); f1(); ~/workspace/test3 via 🐪 v5.34.0 ❯ perlimports --libs lib,local/lib/perl5 -f hoge.pl use strict; use warnings; use FindBin (); use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite (); f1();
これを回避するには、調整したくないモジュールをuse
している行の後ろに特定のアノテーション(## no perlimports
)を書くか*3、コマンド実行時の引数でモジュール名を直接列挙、またはモジュール名が列挙されたファイルを渡すことで回避可能です。この指定には正規表現が使用可能です。
作者にPRを送るとignoreするモジュールに追加してくれる様なので、普段使うモジュールで管理されるとまずいものがあればPRを送ると良いでしょう。
~/workspace/test3 via 🐪 v5.34.0 ❯ cat hoge.pl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite ( rw => [qw/ hoge/], ); f1(); ~/workspace/test3 via 🐪 v5.34.0 ❯ perlimports --libs lib,local/lib/perl5 --ignore-modules-pattern '^Class::' -f hoge.pl use strict; use warnings; use FindBin (); use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite ( rw => [qw/ hoge/], ); f1();
メソッドの追加などの副作用で持つモジュールをuse
している場合は、--no-preserve-unused
に気をつける
Perlのモジュールでは、use
すると自分以外のモジュールのメソッドをオーバーライドやメソッドを名前空間に追加するものがあります。
例えばHTTP::Message::PSGIは、use
するとHTTP::Request名前空間にto_psgi
がメソッドとして追加されます。
これはuse
することで効果を発揮するモジュールであるため、HTTP::Message::PSGIがEXPORTしているメソッドを直接使っていないケースや、HTTP::Message::PSG->
で何かを呼び出しているコードが無い限り、perlimportsで--no-preserve-unused
オプションを付けて実行してしまうと、何も意味がないモジュールを呼び出しているとuse
を削除されてしまいます。
実際はHTTP::Message::PSGI
はperlimports側で明示的に整形する対象に含まないように指定されているので、この様なことは起きないのですが、自分たちで書いているライブラリに似た挙動があるものがあれば、使用する際は気をつけましょう。
明日はid:mp0liiu さんで、immutableなコレクションを作るです!
*1:動的ロードなどを考慮するとこの限りではない
*3:ブロック的に囲うことも可能です
https://metacpan.org/pod/perlimports#ANNOTATIONS/IGNORING-MODULES