概要
動的型付き言語であり中でも自由度の高い Perl だが、それでもコードの堅牢性をなるべく確保しできる範囲で質を上げる方法を考えてみようというお話。アプリケーションを構築する際にある程度実用的だと思われる手法のみまとめ、便利だけれども超変態的なモジュールなどは外している。
注意 個人的な考え方・手法のメモに過ぎないので、すべてをそのまま導入するのはキケンです。まずは読み物としてどうぞ。中級者向け。
なお、Perl に興味ある方には Perl 入学式 という有志の入門者向け定期イベントがあります。
アサーション(表明)を活用する
他の(特に静的型付き)言語ではよく言及されるのに Perl で話題になることはあまりない、アサーション。堅牢なプログラムを書くには欠かせない道具で、端的にはプログラムに埋め込む小さなテストと言える。Carp::Assert
モジュールを使う。
assert()
の中に、結果が真となる条件を記述する。この場合なら、「$x
は 100 より大きい(はず)」と表明したことになる。なお後ろに続くメッセージ文字列はオプションなのでつけなくてもよい。
use Carp::Assert;
my $x = 42;
assert( $x > 100, "x must be larger than 100" );
このように書くと、もし $x
が 100 以下なら実行時にこの行で例外が発生する。具体的にはあらかじめ指定したメッセージとともに Carp::confess()
が呼ばれプログラムが停止する。
Assertion (x must be larger than 100) failed!
....
Carp::Assert::assert('', 'x must be larger than 100') called at ./assert.pl line 15
つまり、「こうあるべき」という「状態/条件」をあからじめ「表明」しておくことで、実行時に予期しない状態になっていることに気付くことができる(プログラムは止まるが)。
通常は、開発中はアサーションをオンにして意図しない動作を検知するのに役立て、本番系では環境変数 PERL_NDEBUG
をセットしてアサーションをオフにする。するとassert()
内に書いたコードは実行されないので、速度面でのペナルティも発生しない。
PERL_NDEBUG=1 ./prog.pl
アサーションの仕組み自体は単純だが、「副作用を起こすようなコードを指定しない」を徹底しないと思わぬ罠にはまるので注意すること。つまり、以下の例で init_something()
が(正しく動けば)真を返すからといって次のようなコードを書いてはならない:
assert( $obj->init_something ); # Bad!
PERL_NDEBUG
に真をセットして実行すると init_something()
メソッドは決して呼ばれない。それに気付かず本番系に投入すると痛い目に遭うので、あくまでも「神の視点でこっそり値を覗き見」るだけにすること。
ほかにもいくつか便利な書き方がある。2つの値を比較するだけなら should()
のほうがよい。affirm()
を使えば複数行にわたる複雑なアサーションも記述もできる。
should( $this, $that );
shouldnt( $this, $shouldntbe );
affirm {
my @cards = @{ $customer->credit_cards };
$cards[0]->is_active; # 最後の式が返す真偽値で成否判定
};
また、ドキュメントを見ると if DEBUG
があちこちに出てくるが基本的にアサーションのオンオフは環境変数を使ったほうがよいだろう。Carp::Assert::More
という便利ヘルパーモジュールもある。
不要な変数をクリーンアップする
Test::Vars
は、コードのなかで定義したはいいものの一度も使用されず「浮いて」しまっている変数を教えてくれる。次のようなケースをきっちり拾ってくれるのでとてもありがたい。未使用の変数があることで:
- 実装漏れに気付ける(あとで実装するつもりだったのに忘れていた、など)
- コーディングミスに気付ける(コピペでコードを移植して改変した時に変数名を間違えた、など)
use Test::Vars;
all_vars_ok(
ignore_vars => [ qw($class) ],
);
done_testing;
基本的に CPAN モジュール作者のためのモジュールのようなので all_vars_ok()
を使う場合には MANIFEST
ファイルが必要(vars_ok()
を使えばファイル指定もできる)。とりあえず適当に用意してからテストを実行する。
perl -MExtUtils::Manifest -e 'ExtUtils::Manifest::mkmanifest()'
prove -v t/vars.t
# $total is used once in &Foo::calc at lib/Foo.pm line 27
ただし、現時点ではサブルーチンリファレンスの中を精査することはできないようだ。これは例えば Moose
環境で after
や override
などのメソッドモディファイアを積極的に使っている場合に特に問題となる。
after 'run' => sub {
my $self = shift;
...
my $p = 10; # 検出できない
return;
};
このように環境によっては効果は限定的だがプログラム的に検出できるのはじつに強力で頼もしい。作者は gfx 氏。
メトリクスを計測する
Test::Perl::Metrics::Lite
を使えばメトリクスに基づいたテストができる。簡単に言うと、複雑過ぎる(=長すぎる)サブルーチンがあれば教えてくれる。コードを副作用のない小さなサブルーチン/メソッドに分割して管理(divide and conquer)する習慣をつけるのに有用だ。
use Test::Perl::Metrics::Lite -mccabe_complexity => 25, -loc => 100;
all_metrics_ok();
どの程度の complexity
が妥当かは最終的には好みによると思うが、30 あたりから始めるとよい。なお Test::Perl::Metrics::Simple
という同等モジュールもあるが、ひとまずこちらを推しておく。
既存のプロジェクトに放り込むと(たいていは)修正点が多くて大変なので、新規プロジェクトから徐々に導入することをお勧めする。
メソッド引数の型をチェックする
Smart::Args
を使うと、Moose
的な型を元にサブルーチン/メソッド引数の型チェックができる。
use Smart::Args;
sub calc {
args my $self,
my $p => { isa => 'Int', optional => 1 };
...
}
Moose
と書式を共通化できるので見通しがよい。作者は tokuhirom 先生。内部実装として Mouse
を利用しているところを Type::Tiny
など汎用性を高めてもらえれば実用したい。
autobox を使う
autobox
モジュールはブラックマジック臭が漂うからか敬遠するひともいるが、思考の流れのままに処理を書けるのはコーディングミスを減らすチャンスにつながる。
use autobox::Core;
# 伝統的な書き方は、右からコードを追わないといけない
say join ', ', grep { $_ > 10 } map { $_ * 2 } 1 .. 10;
# autobox による自然な流れ
1->to(10)->map( sub { $_ * 2 } )->grep( sub { $_ > 10 } )->join(', ')->say;
ちなみにこういった方向性で perl そのものを拡張する perl5i というプロジェクトもある。Moose
環境下なら Moose::Autobox
をどうぞ。
変数をイミュータブル(不変)にする
オブジェクトのフィールドならひとまず Moose
を使えば済む。
has 'timeout' => (
is => 'ro', # readonly
isa => 'Int',
default => 15,
);
加えてメソッド内で一時的に使う変数もイミュータブルにできれば、関数型言語的アプローチを自らに強制できるので、うまく使えば状態管理が明快になりバグを減らせると期待できる。
Data::Lock
を使えばあとから変更できない変数を定義できる。次の例では、10 を代入しようとすると実行時エラーとなる。
use Data::Lock qw(dlock);
dlock my $sv = 42;
$sv = 10; # 再代入不可!
Modification of a read-only value attempted at ./dlock.pl line 15.
Readonly
モジュールを含め類似モジュールはいくつかある。Data::Lock
は XS で内部のフラグを立てる実装なので実行時の速度ペナルティはほぼ無視できる。また Readonly
モジュールと比べるとわずかだが簡潔に書ける上に、オブジェクトごとイミュータブルにできる点もおもしろい(pod 参照)。ただし対象はリファレンスを含むスカラー値のみ。
Attribute::Constant
Data::Lock
に同梱される Attribute::Constant
を使うと生 Array/Hash にも対応できる。
my @av : Constant( value1, value2, ... );
my %hv : Constant( key => value, key => value, ... );
ただし上記のように「生の」値を渡す必要があり、あとからロックすることはできない。
my @array = 1 .. 10;
my @av : Constant( @array ); # 空になってしまう
ともに作者はハッカー/ブロガーとして有名な弾氏だが、実験的実装の意味合いが強そうなので本番系での使用を検討する際には吟味したほうがいいかもしれない。
真偽値型を導入する
boolean
で真偽値型を Perl にも。
use boolean;
my $face_is_handsome = true;
do &something if isTrue($guess);
何よりコードが読みやすくなるし、「Perl には true/false が無いんだぜ」などと言われなくなる。作者は重鎮 Ingy döt Net 氏。
私も昔は利用していたが今は利用していない。ほとんどの場合は問題ないが、たとえば Text::Xslate
などのテンプレートエンジンに true/false 値(実体はオブジェクト)をそのまま渡して制御構文の判定値として使うと動作がおかしくなることがあったため。標準でない型を常用するのはそれなりにリスクがある。
集合型を導入する
Set::Object
で集合型を Perl にも。
use Set::Object qw(set);
my $set = set();
$set->insert( qw(apple orange banana) );
$union = $set1 + $set2;
$intersection = $set1 * $set2;
$difference = $set1 - $set2;
$symmetric_diff = $set1 % $set2;
集合型とは一意な値の集まりで、その要素が重複していないことが保障される入れ物。配列と違い順序はない。たまに Perl で配列(同士)を uniq
したり union
するにはという FAQ を見かけるが、集合型を使うとプログラムの見通しがよくなる。
XS で実装されているので高速だがスレッドセーフではない。MooseX::Types::Set::Object
もある。
サブルーチンリファレンスに名前を付ける
Sub::Name
を使ってサブルーチンリファレンスに名前を付けると、例外が発生した場所を特定しやすくなる。
通常だと、次のようにどれも __ANON__
と表示され区別がつかない。
use Carp;
my $foo = sub { croak "oh!" };
my $bar = sub { $foo->() };
$bar->();
oh! at ./sub.pl line 37.
main::__ANON__() called at ./sub.pl line 38
main::__ANON__() called at ./sub.pl line 40
そこで Sub::Name
を使うと、スタックトレースに固有の名前を表示できるので階層が深い場合にデバッグしやすくなる。
use Carp;
use Sub::Name;
my $foo = subname foo => sub { croak "oh!" };
my $bar = subname bar => sub { $foo->() };
$bar->();
oh! at ./sub.pl line 35.
main::foo() called at ./sub.pl line 36
main::bar() called at ./sub.pl line 37
コードそのものは少し見にくくなるので、ここぞという時にのみ使うのがよさそうだ。
コメント行をデバッグに活用する
Smart::Comments
を使えば、コメント行を拡張してデバッグメッセージを埋め込んだり進行状況を表示したりいろいろできる。直接コードの質に影響するわけではないが、うまく使えば心理的な負担を下げることができるかもしれない。
use Smart::Comments;
### got: $var
for (my $i=0; $i<$MAX_INT; $i++) { ### Working===[%] done
do_something_expensive_with($i);
}
まとめ
- Perl も少しなら他言語っぽく拡張できる。
- 自分のスタイルに合わせていくつかの手法を導入できれば、コード品質を向上できるかもしれない。