モデルベースなどについて分かり易く解説されているツールベンダーのブログがありました。
株式会社スムーズワークスです。
分かり易いのは、ざっくり書いているから、というのもあるでしょうが、
日本語自体がとても魅力的で見習いたいと思いました。
以下、読んだ記事
実行可能な仕様
モデルベース開発は有用なのか?
機能安全セミナー in ESEC2010
一通りは読みましたが、実際に自分の言葉でアウトプットしないと、抜け漏れがありまくるのは、Perlベストプラクティスで実証しちゃってるので、次の機会にブログに書きたいと思います。
2010年9月28日火曜日
モデルベース開発の分かり易い説明
2010年9月27日月曜日
Perl best practices[Perlベストプラクティス] 13章 エラー処理 13.11~13.3
例外が2つ以上関与する場合は、例外オブジェクトを利用する
文字列ベースの例外には、既存の例外から、新しい例外を簡単に作成する方法がない。
例えば、特定の範囲外の整数を警告する例外と、あまりにも大きすぎる整数を警告する例外があるとする。
これを例外オブジェクトを用いた場合、
#以下のような文を含むクラスがあったとする croak( X::TooBig->new( {num=>$num, list=>$MAX_ALLOWED_VALUE} )) if $num > $MAX_ALLOWED_VALUE;
以下のように派生クラスを作成することで、
package X::WaaayTooBig; use base qw( x::TooBig ); #略 croak(X::WaaayTooBig->new( {num=>$num} ) ) if $num > $MAX_INT;
どちらの例外の場合でも、同じコードで補足できる。
つまり派生した例外を作成しても、例外を処理するコードを変更する必要がない。
if( X::TooBig->caught() ){
}
例外クラスは派生端から順に補足する
if( X::TooBig->caught())とした場合、X::WaaayTooBig例外が送出された場合、真となる。
X::waaayTooBigはX::TooBigを継承しているから。
例外クラスを自動的に構築する
例外クラスは便利で保守性が高いが、コードを書くのが難しい。
Exception::Classモジュールを使用するとよい。
2010年9月23日木曜日
Perl best practices[Perlベストプラクティス] 13章 エラー処理 13.7~13.10
エラーメッセージは「何が、なぜ、どこで、いつ」を書く
サブルーチンを使用する側にたって、メッセージを書くこと。そっけないメッセージはダメ。問題の全容、その理由、発生した場所、呼び出し元での失敗したソース行を示すこと。
エラーメッセージの分かり易い解説をドキュメントに書く
コードから生成される可能性のある例外、警告をドキュメントにすることが大事。perldiagの標準マニュアルは平易な文章でエラー内容を記述してあり、お手本になる。例外オブジェクトを使用する
perl5.005以降では、ブレスされた参照(オブジェクト)をdieまたはcroakに渡せるようになった。オブジェクトを例外として使用する利点は2つある。
1つ目は、例外オブジェクトを型で検出できること。例外クラスのcaught()メソッドを使用する。
#get_number()内で以下のような記述があるとする
#X::TooBigという例外クラスをcroakに直接わたす。
#croak(X::TooBig->new( {value=>$num, range=>[0, $MAX_ALLOWED_VALUE]} ) )
#if $num > $MAX_ALLOWED_VALUE;
my $value = eval { get_number() };
if ($EVAL_ERROR) { #例外を補足。例外には例外オブジェクトが入っているかもしれない。
if ( X::TooBig->caught() ) {#例外オブジェクトがX::TooBigクラスに属している
my @range = $EVAL_ERROR->get_range();
$value = $range[-1];
}
elsif( X::TooSmall->caught() ) {#例外オブジェクトがX::TooSmallクラスに属している
$value = $EVAL_ERROR->get_value();
}
}
else {
croak( $EVAL_ERROR);
}
#例外オブジェクト内のcaughtメソッドを抜粋
sub caught {
my ($this_class) = @_;
use Scalar::Util qw( blessed );
return if !blessed $EVAL_ERROR; #EVAL_ERRORはオブジェクトでないならfalseをリターン
return $EVAL_ERROR->isa($this_class); #$EVAL_ERRORがオブジェクトで、かつ、このクラスに属しているなら真
}
二つ目のメリットは、複雑なデータ構造を例外オブジェクトに詰め込んで例外ハンドラに渡せること。
例外ハンドルには、実際のハンドルが含まれているため、再試行できる可能性がある。
croak( X::EOF->new({ handle => $fh }) )
if $fh->eof();
2010年9月22日水曜日
例外クラスがやっとわかった・・・・
例外クラスがわからない、という記事を書いていたら急にわかった・・・
明日書きます。疲れた・・・。
はじめてのPerlも読んでるんですが、図書館から借りてる本(パケット解析)も読まないといけないので悩む。パケット解析の本は、最初の基礎編の章をよんで、OSI参照モデルと、ハブやスイッチ、ルータの特徴を復習しました。
自分は組込み屋なんで関係ないんですが、まぁ趣味・・・ってことで。
仕事に関係あることも勉強しないとなぁ・・・電子回路とか。
2010年9月21日火曜日
初めてのPerl 第5版 読み始めました
perl best practicesの13章を記事にするのに手間取ってて、現実逃避に読んでみたり。
知らないこともあって、やはりこういう本はちゃんと読まないとなーと思いました。
まだ40ページほどしか読んでないですが、知ってる内容が多いのですぐに読めるとは思います。
(自分は読むのが遅いタイプなので、知ってる内容でも1頁30秒はかかるのが辛い)
あと、ラクダ本も買ったけど、二部構成なの知らなくて後半だけ買ってしまったw
1部も買わないと・・・。
2010年9月19日日曜日
Perl best practices[Perlベストプラクティス] 13章 エラー処理 13.1~13.6
2つの原則
- 検出可能なランタイムエラーをすべて検出し、分類し、報告する
- 検出したエラーを無視できないようにする
失敗したときは、フラグなどの値でなく例外を送ることでプログラムを終了させる。
呼び出したサブルーチンが失敗した場合、そのサブルーチンがエラーを表す値をreturnしても、呼び出し元がそのエラーを無視したり、適切に処理できなければ意味がない。
フラグ等を使用することの欠点は2つある。
1つ目は、開発者(サブルーチンを使用して、呼び出し元コードを欠いている人)がエラーを無視することに慣れること。
2つ目は、何重にもネストして呼び出されたサブルーチンは、一番最初の呼び出し元まで、バケツリレーのように、エラーを適切に処理&上位に送信、を繰り返さなければならない。これでは、すべてのサブルーチンのコードが肥大することになる。バケツリレーでエラーを運ぶのではなく、失敗したサブルーチン内で例外を出すこと。
この例外をスルーするにはevalを明示的に使用する努力が呼び出し元に要求される。そして、この例外を一番先頭の呼び出し元で補足することで、各ルーチンすべてにバケツリレー処理を書く必要がなくなる。バケツリレー処理を書かないで済む理由は、例外は自動的にバケツリレーをして、上位呼び出し元に運んでくれるから。そして最上位呼び出し元で、明示的に例外が補足されないとプログラムは終了する。
以下のように、ネストではなく、並列に並んだ処理のすべての値をチェックしたい場合も、例外を使用したほうがいい。
コードがすっきりする。
SOURCE_FILE:
for my $filename (@source_files) {
my $fh = locate_and_open($filename);
next SOURCE_FILE if !defined $fh; #このエラー処理を無くしたい
my $head = load_header_from($fh);
next SOURCE_FILE if !defined $head; #このエラー処理を無くしたい
print $head;
}
for my $filename (@directory_path) {
#例外が出されたときは、evalによって無視されるので、nextと同じ動作になる。
eval {
my $fh = locate_and_open($filename); #サブルーチン内でエラー処理。その際、例外を出すようにする。
my $head = load_header_from($fh); #サブルーチン内でエラー処理。その際、例外を出すようにする。
print $head;
}
}
組み込み関数のエラーで、例外を出させるようにする
Fatalモジュール
組み込み関数や、サブルーチンの名前を指定すると、falseを返すのではなく、例外が出されるように黒魔術がかけられる。
その際、:voidという特殊なマーカを使用しない。これは、voidコンテキスト以外ではFatalが例外を送出しないようにするためのマーカ。紛らわしいので使わない。
やっかいなsystem組み込み関数
systemコマンドは、成功するとfalseを返し、失敗するとtrueを返す。
これはFatalも効かない。POSIX標準モジュールのWIFEXITEDサブルーチンを使用するか、Perl6モジュールを使用する。(Perl6では、system関数は成功するとture、失敗するとfalseを出すように修正される)
use POSIX qw( WIFEXITED );
WIFEXITED(system $cmd)
or croak "Couldn't run: $cmd ($OS_ERROR)";
use Perl6::Builtins qw( system );
system $cmd
or croak "Couldn't run: $cmd ($OS_ERROR)";
回復可能なエラーでも例外を送出する
回復可能なエラーではundefを使用した方が良いと思われるが、ここでも例外を送出したほうが良い。理由は、今までと同じで、開発者がエラー処理をするとは限らないから。(フラグ等、戻り値は無視される)
例外は呼び出し元から出す
サブルーチン内でエラーを返されても役に立たない。呼び出し元からエラーを送出すること。
理由は、あなたが作ったサブルーチンを使用している開発者が知りたいのは、あたたのコードのどこで問題が検出されたのかではなく、自分たちのコードのどこに問題があるか、だから。
dieではなくcroak( )を使用する
呼び出されたサブルーチンの立場でエラーを報告するdieを使って例外を出すのは、呼び出されたサブルーチンに落ち度があるときだけ。そのときのメッセージは常に"Internal error:"で始まるようにすること。その他の場合はcroak( )を使用すること。
しかし、内部エラーであってもcroak( )を使用したほうが良いときもある。例えば、内部エラーを呼び出し元の視点に立って報告することが出来る点。さらに重要なのはCarpモジュールにコマンドラインから使用できる'verbose'オプションがあること。このオプションを使用してプログラムを実行すると、croak( )の呼び出しごとに、エラーメッセージと完全なバックトレースが提供される。
組み込みwarnではなく、carp( )を使用する
dieと同じ理由。警告でもcarp( )を使用すること。
2010年9月16日木曜日
qr{ }のネストについて
#数字を補足する正規表現$NUMBERを作成する
Readonly my $DIGITS => qr{ \d+ (?: [.] \d*)? | [.] \d+ }xms;
Readonly my $SIGN => qr{ [+-] }xms;
Readonly my $EXPONENT => qr{ [Ee] $SIGN? \d+ }xms;
Readonly my $NUMBER => qr{ ( ($SIGN?) ($DIGITS) ($EXPONENT?) ) }xms;
このネストが、なぜ遅いのか理解できました。
まず、なぜ理解できなかったのかというと、$DIGITS => qr{ ... } の時点で、$DIGITSにはプリコンパイルされた値が入っているというのが分かってなかったからです。
$NUMBERで使用している$DIGITSなどはすでにコンパイルされているので、これを逆コンパイルして、文字列にもどしてから、$NUMBERに代入される正規表現全体で再度コンパイルしています。
「逆コンパイル」ということは、そりゃぁ、元の文字列にならないこともあるよなぁ、という感じです。
Deparseしたら見れるのかな?と思って
$perl -MO=Deparse,-x7 qr.pl
としてみたけど。特に何も見れませんでした。
ついでに以下ような変換が見れました。
#before
if(A =~ B){ ... }
#after
A =~ B and do{ ... }
#before
for (my $i=0; $i<10; $i++) {
$i++;
}
#after
while ($i < 10) {
++$i;
}
continue {
++$i
}
初めcontinueは何だろうと思ったけれど、forはブロックの最後に反復子がインクリメントされるから最後に1回だけインクリメントしないといけないですよね。
面白いなぁ。
2010年9月14日火曜日
Perl best practices[Perlベストプラクティス] 12章 正規表現 12.22~12.24
バックトラックを防ぐリファクタリング
共有部分を括り出す
m{
with \s+ each \s+ $EXPR \s* $BLOCK
| with \s+ each \s+ $VAR \s* in \s* [(] $LIST [)] \s* $BLOCK
| with \s+ [(] $LIST [)] \s* $BLOCK
}xms
上記のコードは頻繁にバックトラックが起こる。例えばeachのeでマッチングの失敗したとすると、2つ目の選択肢に移り、またwith \s+でマッチングを試み、そしてまたeachで失敗して、3つ目の選択肢でまたまたwith \s+でマッチングしようとする。
この手の正規表現をPerlが内部的に最適化してくれると良いけれど、それをPerlに任せると処理コストが高すぎるので、自分でリファクタリングする必要がある。
リファクタリングの内容は、共通パターンを括り出すとことだが、実際の手順としては、まず選択肢の集合を(?:)で囲む。そして、共通部分を外に出す。この2ステップを踏む。
m{
with \s+ each \s+ $EXPR \s* $BLOCK
| with \s+ each \s+ $VAR \s* in \s* [(] $LIST [)] \s* $BLOCK
| with \s+ [(] $LIST [)] \s* $BLOCK
}xms
#まず選択肢集合を括弧で囲む
m{
(?: with \s+ each \s+ $EXPR \s* $BLOCK
| with \s+ each \s+ $VAR \s* in \s* [(] $LIST [)] \s* $BLOCK
| with \s+ [(] $LIST [)] \s* $BLOCK
)
}xms
#共通部分を外に出す。頭のwith \s+ とお尻の\s+ $BLOCK。
m{
with \s+
(?: each \s+ $EXPR
| each \s+ $VAR \s* in \s* [(] $LIST [)]
| [(] $LIST [)]
)
\s* $BLOCK
}xms
#まだeach \s+が括り出せるので、括弧で囲む。
m{
with \s+
(?:
(?: each \s+ $EXPR
| each \s+ $VAR \s* in \s* [(] $LIST [)]
)
| [(] $LIST [)]
)
\s* $BLOCK
}xms
#each \s+を外に出す。
m{
with \s+
(?: each \s+
(?: $EXPR
| $VAR \s* in \s* [(] $LIST [)]
)
| [(] $LIST [)]
)
\s* $BLOCK
}xms
この方法の欠点として可読性の低下が挙げられるが、正規表現を定数に変換するなどして対処すること。
(?>)でさらにバックトラックを無くす
先ほどの、選択肢集合から共通部分を括り出す方法は、外に出されたプレフィックスに関しては当然バックトラックは起こらないけれども、外に出したサフィックス(接尾辞)でマッチングに失敗した場合は、選択肢集合にバックトラックして、選択肢集合の中でありとあらゆる可能性を走査するため、バックトラックが頻発する。もし、マッチングが最初から最後まで成功するか、まったくしないかの2パターン(相互排他関係)ならば、サフィックスでマッチングに失敗した際に、バックトラックする必要はない。このことをPerlに教えるために(?>)を使用する。
先ほどの例では、(?:)を(?>)に置き換えるだけでいい。
この(?>)の説明で紹介されている例が興味深かった。それは、ほぼ正しい入力によって、正規表現のマッチングが失敗する場合は、バックトラックが頻発すること。紹介されている例は、コンマで区切られたリストとマッチする正規表現でした。
my $str =~ m{ [(]
$ITEM #少なくとも1つは要素があるようにする
(?: #コンマと要素のペア。
,
$ITEM
)*
[)]
}xms;
このとき、入力されたリストが閉じ括弧が無かったらどうなるか。まず[)]でパターンマッチに失敗した後、前に成功したマッチ(?:,$ITEM)*の最後の繰り返し部分、つまり、リストの最後のコンマと要素の組1つとのマッチ成功を捨てて、その分を[)]とのマッチに使用する。しかし、このマッチは当然失敗する。そうすると、バックトラックして、また一つ前のマッチ成功を捨てて[)]とのマッチを試みる。これは、リスト要素の数だけ繰り替えされる。
これを防ぐために、(?>)を使用するが、先ほどの説明通りに、単純に(?:)を(?>)に置き換えてはいけない。なぜなら*を含めて、パターンの方法は1つとしなければならないから。(?> (?: , $ITEM)* )とする。
まとめ。2つの部分パターンXとYが、照合する文字列に対して相互排他の関係にあるならば、
- X | Y は (?> X | Y )
- X* Y は (?> X*) Y
文字列比較
固定文字列との照合は|ではなく、個別にeqする
初めから照合したい文字列が決まっている場合、(?: A | B | C)より、($hoge eq 'A') || ($hoge eq 'B') || ($hoge eq 'C')のほうが20%程高速であるし、コードも読み易くなる。固定文字列の数が多い場合や、数が決まっていない場合
選択肢の数が多い場合や、数が決まっていない場合は、正規表現を生成するほうが適していると思うかもしれないがeqを使用したほうがすっきりする。ただし低速。
Readonly my $EXIT_WORD => join '|', @EXIT_WORDS;
last COMMAND if $cmd =~ m{\A (?: $EXIT_WORD ) \z}xms;
use List::MoreUtils qw( any );
last COMMAND if any { $cmd eq $_ } @EXIT_WORDS;
今後読む予定の本
順番がおかしいかもしれませんが、ベストプラクティスを読んだあとは、以下の順で体系的に学んでいきたいと思います。
プログラミングPerlを読破した後は、現状では意味不明の実用Perlプログラミングに挑戦してみます。
すぐわかるオブジェクト指向Perlは、初心者向け内容っぽいのですが、ひどいコードからリファクタリングしていく様子がわかるらしいので、ちょっと読んでみたいと思います。たぶん上記の本まで読んでると、多少天狗になってそうな気がするので、基本の本を読んでへし折っとこうと思います。
後は、順不同かな?
モダンPerl入門とデータマンジングとPerl Cookbookとかもありますね。マスタリングPerlも読んだほうがいいのかなぁ・・・。
ベストプラクティスは読むのに1ヶ月くらいかかったので、他のはもっと早く読みたいな・・・
2010年9月13日月曜日
理解できてないなぁ
Perlベストプラクティスの正規表現の部分があんまり理解できてなくて、書くのにすごい時間がかかってます。本自体はどんどん先を読んでいて、もう読み終わりそうなんですが・・・。正規表現あたりまでは黙読だったのですが、それがまたいけなかった感じです。
黙読すると、知らないうちに脳が詳細を理解するのを避けていて、理解した気になって次に進んでしまうようです。そこで、シャーペンで自分の言葉で、本にぐりぐり書くようにしたら、大分、詳細から逃げずに理解できるようになりました。
ただ、それでもブログに書くときになると、解らなかったことがわかるようになるので(どうも集中力が違うみたい)、やっぱりアウトプットは大事だなぁと思いました。
以下の言葉が実感としてわかります。
2.インプットではなくアウトプットに基づいて目標を設定する
「3時間がんばろう」というのは、まやかしです。「3ページ書こう」にしましょう。
文章力向上のための10の基本的心構え
Perl best practices[Perlベストプラクティス] 12章 正規表現 12.18~12.22
テーブルから正規表現を生成する
前節では三項演算子を用いたテーブル(っぽくみえる表記)で、正規表現のパターンマッチを行ったが、テーブルは正規表現を生成するためにも使える。あるテーブルを正規表現を使って検索する場合、テーブルから正規表現を生成する。
#変則的な複数形のテーブル
my %irregular_plural_of = {
'child' => 'children',
'brother' => 'brethren',
'money' => 'monies',
'mongoose' => 'mongooses',
'ox' => 'oxen',
'cow' => 'kine',
'prima donna' => 'prime donne',
'tooth' => 'teeth',
'toothfish' => 'toothfish',
};
#正規表現の生成
my $has_irregular_plural = join '|', map {quotemeta $_} reverse sort keys %irregular_plural_of;
while (my $word = <>) {
chomp $word;
if ($word =~ m/\A ($has_irregular_plural) \z/xms) {
print $irregular_plural_of{$word}, "\n";
}
else {
print form_regular_plural_of($word), "\n";
}
}
ここでは2つの作業が必要になる。
1つ目は、reverseを用いていること。|でつないだ正規表現は、左から順に評価されるので、作成された正規表現が....| tooth | toothfish | ... の順番だと、パターンマッチの部分で、ハッシュテーブルのtoothfishは正規表現のtoothとマッチしてしまう。このため、生成される正規表現は、toothfishがtoothより先に評価されるようにreverseを使用する。
2つ目は、quotemetaを使用することで、メタ文字をエスケープすること。'prima donna'も'prima\ donna'になり、/xフラグがセットされていても問題なく処理できるようになる。
このテーブルを用いた正規表現の生成は、テーブルと正規表現の一貫性を保証できるし、テーブルにキーと値のペアを追加するだけで、正規表現も拡張される点でメリットがある。
決して、以下のように、元のテーブルのキーをコピーしただけの正規表現を作ってはいけない。これは作業が面倒だし、タイプミスも混じり込みやすい。
my $has_irregular_plural = qr{
child | brother | mongoose
| ox | cow | money
| tooth(?:fith)?
}xms;
複雑な正規表現は単純なパーツに分ける
#数字を補足する正規表現$NUMBERを作成する
Readonly my $DIGITS => qr{ \d+ (?: [.] \d*)? | [.] \d+ }xms;
Readonly my $SIGN => qr{ [+-] }xms;
Readonly my $EXPONENT => qr{ [Ee] $SIGN? \d+ }xms;
Readonly my $NUMBER => qr{ ( ($SIGN?) ($DIGITS) ($EXPONENT?) ) }xms;
ここで、一つqrを使った際のデメリットを説明する。それはqrが適用された正規表現内に、qrが適用された正規表現があると、パフォーマンスが低下するかもしれないこと。$EXPONENTや$NUMBERが当てはまる。理由は、要素である正規表現を展開する際に、文字列に逆コンパイルされてから展開され、最後に再コンパイルされるが、文字列に逆コンパイルする際に、最適化されず効率の悪いパターンを生成する可能性があるため。
代わりにq{ }またはqq{ }を使用すると、単純な文字列として、製作者が作成した正規表現がそのまま展開される。ただし、q{ }、qq{ }を使うと、エスケープ文字や、部分パターン全体を一つの項目として扱ったりするので、複雑な場合は、qr{ }を使うことが推奨される。
あまりにも複雑な場合は、Regexp::Assembleを使用してみる。
既存の正規表現をまとめたモジュールを使用する
正規表現の車輪の再発明はとても多い。これはRegexp::Commonモジュールを使用することで楽になる。
前節の、数字にマッチする正規表現は以下のように書ける。
use Regexp::Common;
Readonly my $NUMBER => $RE{num}{real}{-keep};#いつも$REというハッシュを基に作成する。
単一文字を|でつなぐのではなく文字クラスを使用する
以下の1番目のように書くとパフォーマンスの問題が生じる。2番目のようにすること。また、複数文字の共通点を文字クラスに統一することも推奨される。
if($cmd !~ m{\A (?: a | d | i | q | r | w | x ) \x}xms)
if($cmd !~ m{\A [adiqrwx] \z}xms)
2010年9月12日日曜日
日本の Perl ユーザのためのハブサイト
YAPC::Asia 2008 で Michael Schwern は「SEO に有効な独自ドメインを取って、もっと Perl 初心者が集まりやすい nice な Perl の情報を集めたサイトを作れ!」といったので Perl-users.jp ドメインを取って、ここに Perl-users.jp を開始します。(2008年5月20日)
うーん、まだまだ普及してませんね・・・・。
正規表現に範囲演算子を適用する
naoyaグループ - naoyaの日記 - if /regexp/../regexp/
以下、はてなブックマークのmiyagawaさんのコメント
miyagawa "間に含まれる文字列" というわけじゃなくて if $a .. $b で "$a がtrue になってから $b が true になる間まで" になる、けどこんなんめったに使わないな。。 perldoc perlop
Perl best practices[Perlベストプラクティス] 12章 正規表現 12.13~12.17
ターミネータ(区切りとなる文字)がわかっている場合は、.*ではなく補完文字クラスを使用する
.*という正規表現はとても処理が重くなる可能性がある。
例えば以下の例では、最初の.*は隣の%ともマッチし、その後、文字列の終わりまでマッチする。その後、次の正規表現である%を文字列の続きから探そうとするが、すでに.*によって文字列の最後までマッチが進行しているので、%がマッチする箇所は存在せず、マッチは失敗する。このとき、正規表現エンジンは、%がマッチに失敗した箇所から一文字後ろの文字に着目して、マッチを試みる(バックトラック)。マッチが成功するまでこの一文字ずつのバックトラックを繰り返すため、効率はとても悪い。
$sorce =~ m/\A (.*) % (.*) & (.*) /xms;
とりあえず.*を.*?にしてみても、処理は改善されない
このような正規表現を作成して、処理がとても遅い場合、とりあえず.*を.*?にして問題を解決してみようと思うかもしれないけれど、たぶんあまり上手くいかない。
.*?は.*とは違い、マッチする文字列をできるだけ少なくしようとする「けちけちした繰り返し」ではあるが、これは結局、文字の先読みをしているので、ターミネータ(区切り文字。この場合、%と&のこと)が単一文字よりも複雑である場合は、処理が遅くなる可能性がある。
.*と.*?はエラーを隠蔽する可能性がある
正規表現をみる限り、おそらく製作者はデータに%と&が一つずつ含まれていることを想定しているが、%と&が文字列に複数含まれていても.*と.*?はエラーにならない。文字列が...%....%....&...となっていた場合、2回目の%までは、.*か.*?にマッチする。そうしないと、全体でマッチが成功しないから。
補完文字クラスを使用する
上記の理由から、解決策は%や&といったターミネータ以外を表現する正規表現を使用すること。つまり、以下のようにする。
$source =~ m/\A ([^%]*) % ([^&]*) & (.*) /xms;
補完文字クラスを使用することにより、.*?の先読みも、.*のバックトラックも行われない。かつ余分なターミネータが含まれていた場合、マッチに失敗するようになる。
また最後の(.*)に着目して欲しい。これは常にマッチに成功するので、バックトラックは行われない。逆に、(.*?)が正規表現の最後にあるのは常に間違いである。(.*?)は「けちけち繰り返し」なので、(.*?)の後に正規表現がないならば、最もケチなのは、まったくマッチをしないことになる。そのため、常にNothingにマッチし、成功する。このため、最後の(.*?)は余計であるか、書き手が(.*?)の挙動を理解していないために、書き手の意図とは異なったものであるか、\zアンカーの付け忘れのいずれかである。(\zアンカーがあれば、(.*?)は文字列の最後までマッチさせる義務が生じる)
補足のための括弧は、補足のためだけに使用する
単純なグループ化のために括弧を使用しない
グループ化には(?:)を使用すること。後方参照する必要がないならば、こっちのほうが、余計な補足をしなくていい。余計な補足は、余計な処理をする以上に、保守をする人間に誤った意図を伝えることが問題となる。例えば、以下の例では、perform_cleanup()サブルーチンの中で補足された文字が使われていないか探し回るハメになる。(そして結局使われていないことがわかる)
if ( $cmd =~ m/\A (q | quit | bye | exit) \n? \z/xms ) {
perform_cleanup();
exit;
}
(補足した正規表現が、そのままサブルーチンの中で使えるなんて知らなかった)
補足変数$1,$2などは、パターンマッチが成功した場合にのみ使用する
パターンマッチが成功したかどうかは、$1,$2などが定義されているかをチェックするのではなく、常に以下のようにパターンマッチ全体が成功したかどうかを評価すること。
if ($full_name =~ m/\A (Mrs?|Ms|Dr) \s+ (\S+) \s+ (\S+) \z/xms) {
($title, $first_name, $last_name) = ($1, $2, $3);
}
なぜなら、パターンマッチに失敗した場合、$1などの変数には何も代入はされないが、以前に成功したパターンマッチの結果が保持されているから。
補足変数をそのまま使用せず、適切な名前を付ける
$1,$2などは$_[0],$_[1]などと同じく、何も意味のない値で保守に苦労する。また$1と$2の間に新しく補足変数を追加した場合、従来の$2は$3になるため、その後のコードをすべて書き直す必要がある。
このようなことをしないためにも、適切な名前をつけた変数に代入するようにすること。この際、リストコンテキストを用いて補足した値を直接リストに渡すことができる。これは、常に補足した値をすべて返すため、変数に代入し忘れることがない点で推奨される。
my ($opt_name, $operator, $opt_val, $comment)
= $config =~ m/\A (\S+) \s* (=|[+]=) \s* ([^;]+) ; \s* \# (.*)/xms;
ただし、次節で説明するように、/gc修飾子を使用する場合は適していない。
トークンごとのパターンマッチ
発見したトークンを置換で文字列から削り取るのは処理が遅い
文字列をトークンに分解する際には、以下のように少しずつ文字列を削り取っていくのが常套手段だが、処理が遅くなってしまう点が良くない。
while (length $input > 0) {
if ($input =~ s{\A ($KEYWORD)}{}xms) {
my $keyword = $1;
push @tokens, start_cmd($key_word);
}
else if ($input =~ s{\A ($IDENT)}{}xms) {
my $ident = $1;
push @tokens, start_ident($key_word);
}
else if ($input =~ s{\A ($BLOCK)}{}xms) {
my $ident = $1;
push @tokens, start_block($key_word);
}
else {
my ($content) = $input =~ m/ \A ([^\n]*) /xms;
croak "Error near: $content";
}
}
削らずに前回位置を記憶するだけにする
このため、マッチした部分を置換によって削るのではなく、/gcフラグをセットし、前回マッチした位置を記憶し、組み込み関数pos()を使用して、記憶した位置から次のトークンを探すようにする。/gは前回位置を記憶し、/cはマッチに失敗しても位置記憶(前回マッチに成功した位置)をリセットしないようにする。
cフラグを伴って\Gを使う局面は、典型的にはtokenizerのようにあるマッチが失敗したときに別のマッチを試したいときです。
perlfaq6 - Regular Expressions ($Revision: 1.31 $, $Date: 2005/03/27 07:17:28 $) 訳出 2005/11/2
さっきのコードと違うところは、while文の終了条件にposを使用していること。\A(削り取った文字列の先頭)が\G(前回マッチした終わりの位置)になっていること。置換は必要ないので、s{ }{ }ではなくm{ }であること。
pos $input = 0;
while (pos $input < length $input) {
if ($input =~ m{\G ($KEYWORD) }gcxms) {
my $keyword = $1;
push @tokens, start_cmd($key_word);
}
else if ($input =~ m{\G ($IDENT)}gcxms) {
my $ident = $1;
push @tokens, start_ident($key_word);
}
else if ($input =~ m{\G ($BLOCK)}gcxms) {
my $ident = $1;
push @tokens, start_block($key_word);
}
else {
my ($content) = $input =~ m/\G ([^\n]*) /gcxms;
croak "Error near: $content";
}
}
三項演算子の正規表現版
この場合、条件文が同じ@tokensに値を設定しているので6章で説明した三項演算子が有効である。
#略
push @tokens, (
$input =~ m{ \G ($KEYWORD) }gcxms ? start_cmd($1)
#略
gcフラグはリストコンテキストで使わない
/gフラグはスカラコンテキストとリストコンテキストでは動作が異なる。リストコンテキストでは、正規表現にマッチした部分文字列がリストとして返されるようになる。
2010年9月11日土曜日
Perl best practices[Perlベストプラクティス] 12章 正規表現 12.1~12.12
問題にぶつかると「正規表現を使えばいい」と考える人がいる。
そして、問題を2つ抱えることになる。
--Jamie Zawinski
常に/xフラグを使用する。
これは、確実につかう。JScriptでもXRegExp使ってます。
Xを使うことで、ホワイトスペースが無視され、#も使用できる。そのため、ギチギチに詰められた正規表現である必要がなくて、意味のある単位ごとに、スペースで分けることができる。
もっと分かり易くするには、意味のあるグループごとに改行して、インデントを付け、コメントをつける。
常に/mフラグを使用する
メタ文字^$は任意の行の先頭と末尾にはマッチしない。ほとんどのUnixユーティリティ(sed,grep,awk等)はもともと行思考なので、^$は行の先頭と末尾にマッチするが、Perlではそういった意味はもたない。(JavaScriptでもね!)
Perlでは文字列全体の先頭と末尾を表す。これはプログラマーが通常求める自然な動きではない。/mフラグを使用することで、^$が行単位でマッチするようにすること。
文字列の境界に\A\zを使用する
本当に文字列の先頭と末尾を指定したいときに、/mフラグを外して^$を使用してはいけない。/mは常に使用すること。
代わりに\A\z(小文字のz)が文字列の先頭と末尾を表現してくれる。\A\zは/mフラグの有る無しに関わらず使用することができる。
\Z(大文字)を使わない
\Zは行ベース入力の際に便利だが、\z(小文字)と見分けが付きにくいので、使用しない。\Zは改行とそれに続く末尾にマッチするので、\n? \z を代わりに使用すること。
常に/sフラグを使用する
衝撃的なことにドットは改行にはマッチしない(他の言語でもそうだけど)。これは自然なプログラマの意図とも外れるし、^$との併用でも弊害がでる。例えば{\A (.*?)^}としても^の前は改行であるはずなので、ドットとはマッチしない。
/sフラグを使用することで、ドットは改行とマッチするようになる。本当に改行とマッチさせたくない場合は[^\n]*とすればいい。
自動的に/xmsフラグを挿入する
Regexp::Autoflagsを用いる
正規表現が複数行にまたがる場合は、/.../ではなく、m{ }を使用する
//を使用すると、正規表現が見にくくなる。またxフラグを使用した場合に、問題が生じる。
以下の正規表現はうまく動かない。
m/
set \s+ #キーワード
($IDENT) \s* #Name of file/option/mode
= \s+ #リテラルの=
([^\n]*) #Value of file/option/mode
/xms;
理由はコメント文だと思われている#Name of file/のスラッシュが正規表現の終わりだと判断されているため。コメントはパーサが正規表現を終了したと判断した後に、初めてコメントになる。
{}を正規表現の開始終了のデリミタとして使用すること。{}を使用しても同じ問題が発生するが、コメント文の中に{}が現れる可能性は/よりもずっと低いし、それによって生成されるエラーメッセージはスラッシュのときよりも遥かに分かり易い。
ただし、mapなどのリスト演算ブロックではスラッシュを使用した方がコードが見やすい。
my @counts = map { m{(\d{4,8})}xms } @count_reports; #中括弧が3回もでてくる
my @counts = map { m/(\d{4,8})/xms } @count_reports;
エスケープされたメタ文字ではなく、文字クラスを使用する.
バックスラッシュでエスケープされたメタ文字は、とても見づらい。このため、メタ文字を一文字で構成される文字クラスとして表現する。
\.ではなく、[.]とする。
処理速度に問題がある場合は、名前付き文字を使用する
perlのバージョンによっては、処理速度に問題がある場合があるので、ベンチマークで問題が発覚した場合には、4章で説明した、名前付き文字を使う。
ただし、/xフラグを使用している場合は、名前付きで空白を指定しても、空白は空白なので、ムシされることに注意する。この場合は、文字クラスを使用する。
use charnames qw( :full );
$name =~ m{ harry [\N{SPACE}] s [\N{SPACE}] truman #harry s truman
| harry [\N{SPACE}] j [\N{SPACE}] potter #harry j potter
}ixms;
[A-Z]等、列挙された文字クラスではなく、Unicodeに準拠する名前付き文字クラスであるプロパティを使用する
[a-Z]のような文字クラスはASCIIのアルファベットのみとマッチし、Unicodeのアルファベットにも、Latin-1にもマッチしない。Perl5.6以降の正規表現では\p{}エスケープがサポートされており、Unicodeのプロパティを完全に利用できる。[A-Z][A-Za-z]は、\p{Uppercase}\p{Alphabetic}と表せる。
また言語に依存しないプロパティを使用することもできる。
Readonly my $PERL_IDENT => qr/ [A-Za-z_] \W* /xms;
Readonly my $PERL_IDENT => qr/ \p{ID_Start} \p{ID_Continue}* /xms;
得に便利なプロパティがあり、それはドットメタ文字に対応する\p{any}
コードがとても読み易くなる。
ホワイトスペースが規則通りに入力されることを期待しない
データへの入力としてホワイトスペースが要求されている箇所には\s+を、ホワイトスペースが入力される可能性があるところには\s*を使用する。
2010年9月10日金曜日
Perl best practices[Perlベストプラクティス] 11章 参照
サーカムフィックス(circumfix)逆参照を使わない
サーカムフィックス(circumfix)逆参照とは、コレ=>${$list_ref}[0]
代わりに矢印をつかって、$list_ref->[0] とすること。矢印のほうがすっきり書けるし、2重引用符で文字列に展開できるという利点がある。また、逆参照のほうは、中括弧や、プレフィックス$を忘れてもコンパイルエラーにならないというデメリットがある。
スライスをする場合は、逆参照を使う他ない。
#下記のコードは同じ意味になる。
#$list_refが$で始まるので、[]はスカラーコンテキストで評価される。
my ($from, $to) = $list_ref->[0, -1];
my ($from, $to) = ($list_ref->[-1], undef);
逆参照しか選択の余地がないときは{}で囲む
$$$stack_ref[0]とかはダメ。
シンボリック参照を決して使用しない
シンボルを使うと、型グロブを通じて、現在の名前空間のパッケージ変数を参照する。
パッケージ変数を使用する必要はほぼないはずで、大体レキシカルハッシュで代用できる。
双方向参照のデータ構造では、weakenを使用する
双方向参照では、片方がスコープを外れたりして、削除されても、もう片方から参照されているのでガベージコレクションの対象にならず、メモリが開放されない。
そこで、weakenを使用すると、参照カウントが1減るが、参照自体は残すことができる。片方がスコープから外れると、参照カウンタは0になるので、両方の参照ともに正しく消滅する。
2010年9月9日木曜日
Perl best practices[Perlベストプラクティス] 10章 IO 10.11~10.18
*STDINが本当に必要な時以外は<*ARGV>を使用する。
*STDINは、現在標準入出力に用いているものを常に指すわけではないし、ファイル名を指定しなければ(open *STDIN '<' $filename or croak...)、コマンドラインで指定されたファイルから・・・という意味にはならない。
*STDINは常に0番目のファイルディスクリプタを指す。デフォルトがターミナルであればいいが、パイプやファイルの中身を読んで、内容から他のデータを読み込みにいく等はできない。(そのファイル自体を指す)
普段使用している*ARGVを用いるほうがいい。(*ARGVは<>で表せる)
while (my $line = <>) {
print substr($line, 2);
}
print文では常にファイルハンドルを中括弧で囲む
レキシカルファイルハンドルを中括弧で囲むと、目につき易くなる。
以下のコードが、コンマを忘れただけだと思われないようになる。
print {$file} $name, $rank, "\n";
IO::Handleモジュールを使用してprintサブルーチンを使用する手もある。
対話形式の入力を得る場合には常にプロンプトを表示する
これを実装するのはかなり難しいので、以下のCPANモジュールを使用する。
use IO::Interactive qw( is_interactive );
if (is_interactive()) {
print $PROMPT;
}
プロンプトの表示にはIO::Promptを使用する
対話形式で入力を取得したい場合、IO::Promptを使用すると簡単。
これは質問、回答、検証という一連の処理を抽象化できる。
#通常はコレ
my $cmd = $EMPTY_STR;
CMD:
while ($cmd !~ $QUIT) {
if (is_interactive()) {
print get_prompt_str();
}
$cmd = <>;
last CMD if not defined $cmd;
chomp $cmd;
execute($cmd)
or carp "Unkown command: $cmd";
}
#IO::Promptを使用した場合
while ( my $cmd = prompt(get_prompt_str(), -fail_if => $QUIT) ) {
execute($cmd) or carp "Unkown command: $cmd";
}
$cmd変数のスコープがループ内になり、改行も自動的にchompされる。
対話モード以外の処理がしばらく続く場合は、進行状況をユーザに伝える
これには、Smart::Commentsを使用する。
use Smart::Comments;
for my $possible_confg ( @CONFIG_PATH ) { ### 初期化...終了
init_from($possible_config);
}
my $connection;
TRY:
for my $try (1..$MAX_TRIES) { ### サーバへの接続...終了
$connection = connect_to($REMOTE_SERVER, {timeout=>$TIMEOUT});
last TRY if $connection;
}
croak "Can't contact server ($REMOTE_SERVER)"
if not $connection;
#ここから対話モード開始
このモジュールはwhile,forと同じ行に、###マークが付いたコメントを配置することで、自動プログレスインジケータ(処理中であることを表す表示)を生成することができる。
詳細は18.13節「半自動デバック」参照。
自動フラッシュを設定する場合は、生のselectを使用しない
フラッシュは、バッファにたまったデータを吐き出すこと。
よく使われる下記のコードは保守しにくい。
select ((select($fh), $|=1)[0]);
selectはファイルハンドルをprint文のデフォルトの出力先として使用する。グローバルで。
レキシカルファイルハンドルを使用する場合には、IO::Handleを使用することで簡単に書ける。
use IO::Handle;
$fh->autoflush();
*STDOUT->autoflush();#パッケージスコープの標準ファイルハンドルでも使用可能
Perl best practices[Perlベストプラクティス] 10章 IO 10.1~10.10
ファイルハンドルに裸のワードを使用しない
open FILE '<', $filename
or qroak "Can't open '$filename': $OS_ERROR";
このFILEはシンボルテーブルのエントリ(グロブ)に格納される。
間接的はファイルハンドルを使用する
レキシカル変数にファイルハンドルへの参照を代入することにより、名前の競合が防げるほか、スコープを外れたら自動的にファイルハンドルを自動的に閉じるというメリットもある。
ファイルハンドルはすぐにローカル化する。
理由はいわずもがな。注意点としてはローカル化したあとにネストされたサブルーチンがあると、ローカル化された変数が継続して有効だと言う点。
IO::Fileモジュールを使用するか、引数3つのopenを使用する
まれだが、'>temp.log<'などというファイル名が指定されると、入出力モードにバグが入り易い
open my $active, '<', $ACTIVE_LOG or croak "Can't open $ACTIVE_LOG: $OS_ERROR";
my $active = IO::File->new($ACTIVE_LOG, '<')
or croak "Can't open $ACTIVE_LOG: $OS_ERROR";
結果をチェックせずにファイルへのopen,close,printを行わないこと
必ず毎回チェックを行うこと。
しかし、毎回毎行チェックすると構文が汚れるので、13.2節を参考にFatalモジュールを使う。そうすると、組み込み関数で失敗すると例外が送出されるので、openする度に結果をチェックするコードを書かなくてよくなる。
for(<>)ではなくwhile(<>)を使用する
forの反復リストはリストコンテキストなので、<>演算子がリストコンテキストで呼び出される。すると、可能な限りすべての行を読み取ってから、一時的なリストを作成する。これはすべての行を読み込み終わるまで、forループが開始しないことを表す。
そのため、対話モードで使用することはできないし、メモリも入力に対して200%ほど必要になり、メモリ割り当てやスワッピングで処理速度が話にならないほど低下する恐れもある。
入力を読み取る際はwhileを使用すること。ただし範囲(2..1000)はfor文を使用しても問題ない。
ファイルの丸呑みではなく行ベースのIOを使用する
ファイル内容を丸呑みすると、1行ずつ処理する場合に比べて、時間がかかるし、堅牢性とスケーラビリティに欠ける。丸呑みすることの利点は、ファイルの内容が不安定な場合や、処理が複数行に渡る場合など。
while (my $line = <>) {
$line =~ s/$EXPLETIVE/[DELETED]/gxms;
print $line;
}
丸呑み
my $code = do { local $/; <$in> };
$/は入力のセパレータ文字を指定するが、local化は初期値にundefが入るため、セパレータがundef、つまり存在しないので、行で区切られずに、ファイル内容を丸呑みする。
また、$/の変更の影響を最小限に抑えるためにdoブロックで囲んでおく。
perl6では次のように自然に書くこともできる
use Perl6::Slurp;
my $text = slurp $file_handle;
これはスカラーコンテキストではファイル全体を読み込み、リストコンテキストでは1行ずつのリストとしてファイル全体を読み込む。
また、セパレータに段落や、正規表現を組み込むことも可能になる。
2010年9月7日火曜日
Perl best practices[Perlベストプラクティス] 9章 サブルーチン
サブルーチンは括弧付き、かつ&無しで呼び出す
- サブルーチンは現在の名前空間で定義されている場合は括弧無しで呼び出せる。
- これは覚えておくと良い。裸の文字列がサブルーチンとして認識されうるということ!
- 括弧がないと、組み込み関数と区別しにくくなる
- &はビット演算子でもあるので、構文によっては紛らわしいことになる。
- サブルーチンが引数を取らない場合も括弧をつける。そうすれば型ではないことがはっきりする。obj()->update($status);
組み込み関数と同じ名前を使用しない
サブルーチンと組み込み関数の名前が衝突すると、どちらが優先されるかは、その組み込み関数による。
常に@_を展開する
- $_[1],$_[2]は何を表しているのか謎
- @_はエイリアスなので、レキシカル変数にコピーしておくことで、元の変数をうっかり変更しない
- 1行のリストで展開するのが良いが、例外として、一つ以上の引数を検証する必要がある場合や、インラインコメントを付ける場合は、shiftを使用する。
- 引数の検証にサブルーチンを使う場合は、3章で紹介したように内部処理専用のサブルーチンを表す名前をつける。(頭にアンダーバー)
- エラーの送出は、検証用のサブルーチンを呼び出した側の名前空間になるようにすること。(どこで呼び出されてエラーになったのかが知りたい情報)
- 頻繁に呼び出す場合は、効率のために検証をサブルーチンではなく、インライン展開する。
引数が3つ以上ある場合は、ハッシュを利用する
ハッシュで変数名をキーとして、サブルーチンの引数に渡すと、引数の順番を気にしなくてよくなる。
$line = padded({ text=>$line, cols=>20, centered=>1, filler=>$SPACE });
これを、{}を省略して、ハッシュではなく、名前と値のペアとして渡すと、コンパイル時にエラーが出なくなるので、注意。
通常使用する名前のない引数と、今回の名前付き引数を混在させると、オプション用の引数を明確に分離することができる。
$line = padded( $line, {cols=>20, centerd=>1, filler=>$SPACE} );
sub @added {
my ($text, $arg_ref) = @_;
#以下略
}
引数が欠けているかの検証は、引数の数や、定義されているかで判断する
- 論理演算子を用いた検証は一般に誤り
- 引数の文字が'0'であったり、テキストが空文字だった場合、例外が送出される。
- 引数が定義されているかで評価
- if any {!defined $_} $text, $cols, $filler;
- undefが引数として渡される場合
- definedが使えないので、引数の数が予めわかっている場合は数をチェックする
- 引数の一部がオプションの場合
- オプション引数がハッシュであると、オプションがいくらあっても引数の数としては、1個であるため、チェックがし易い。オプションが完全に省略されていた場合は、カウントされない。
引数のデフォルト値
- ||や||=を使用しない。
- 初期化のコードと、実際に値を使用するコードは分離すること。
- デフォルト値の数が多い場合はテーブルを利用する。
Readonly my %PAD_DEFAULTS => (
cols => 78,
cnterd => 0,
filler => $SPACE,
);
sub padded {
my ($text, $arg_ref) = @_;
my %arg = ref $arg_ref eq 'HASH' ? (%PAD_DEFAULTS, %{arg_ref})
;
#略
このコードのミソは、(%PAD_DEFAULTS, %{arg_ref}) にあって、%PAD_DEFAULTSが%{arg_ref}の前にあるため、まずはデフォルト値で%argが初期化され、次に%{arg_ref}で上書きされる。(ハッシュなのでキーアクセスだから)
このため、arg_refで指定されなかった値にはすでにデフォルト値がセットされる。
スカラーをreturnするときは、常にreturn scalarを使用する。
- returnのコンテキストは呼び出し側のコンテキストで決まる
例えば、以下のように、要素数が知りたいために、スカラーコンテキストでhow_many_defined()を呼び出した際に、誰かが、以下のようにした場合、リストコンテキストとなり、リストの最初の要素が返されてしまう。
my ($found) = how_many_defined(@raw_samples);
戻り値はユーザがマニュアルを読む前に予想するものにする
リストなどと異なり、スカラーは複数の種類(BOOLや数、文字列、参照など)が存在する。この複数ある種類の中から、1つだけを返すことが理想となる。このため、ユーザがサブルーチン名等を見たときに、どんな値がreturnされるのをユーザが期待しているかを考える必要がある。
一人で考えるのは限界があるが、まずは、サブルーチンのカテゴリーをもとに考える。カテゴリーは同種(Cでいう配列)、異種(Cでいう構造体)、反復(readline関数など)。
- 同種
- 同種は各要素が同じ重要度であり、スカラーコンテキストで要求された場合、通常、要素数が要求されているはず。
- 異種
- 重要度が高い要素が含まれていることが多く、スカラーコンテキストではその要素が返されることが期待されているはず。だが、すべて重要だと期待されることもある。(localtimeやgettimeなど)
- 反復
- readlineなどに代表されるように、スカラーコンテキストでは、1回の反復の結果を返すことが要求されているはず。
ユーザが期待する戻り値を特定できない場合は、複数のreturnを準備する
Contextual::Returnを使用する
use Contextual::Return;
sub get_server_status {
my ($server_ID) = @_;
my %server_data
= _ascertain_server_status($server_ID);
return (
LIST { @server_data{ qw( name uptime load users )}; }
BOOL { $server_data{uptime} > 0; }
NUM { $server_data{load}; }
STR { "$server_data{name}: $server_data{uptime}"; }
HASHREF { \%server_data; }
);
}
これほど多くの選択肢を準備する必要がないとしても、Contextual::Returnを使用することで、コードがすっきりするし、今後追加する際の保守性も向上する。wantarrayなどを使用すると、ifの分岐でコードが散乱するが、このモジュールだと、return文の中で表のように記述できるので、各コンテキスト間での違いを際立たせることができる。
サブルーチンプロトタイプを使用しない(一般に誤解を与える)
sub clip_to_range($$@){....}の($$@)は、一つ目の引数がスカラーであることを要求しているように見えるがそうではない。実際は一つ目の引数がスカラーコンテキストで評価されることをperlに要求している。
つまり、clip_to_range(@range,@samples)のような呼び出しをした場合、サブルーチン側で@rangeを元に、2つのスカラー値に分割されたり、スカラーではないことを理由にエラーになるのではなく、配列の要素数が得られることになる。
また、この場合($$@)の@には引数が与えられないことになるが、これもコンパイルエラーにはならない。「空のリスト」もリストだから。
このように、プロトタイプを使用してもコードの堅牢性は大して向上しない。
常に明示的なreturnを使用する
とにかくreturnは最後につけること。そうしなければ、「最後に評価された式」の結果が帰ってくるが、それはperlコンパイラが密かに変換した式で行われたかもしれない。そうなると、帰り値を元に自分で書いたコードを読んでも理解できない。
returnは自動で呼び出し元のコンテキストに応じたfalseを返すので余計な指定はしない
return undef;などと指定すると失敗する可能性がある。それは呼び出し元がリストコンテキストで呼び出された場合。この場合、リストの要素としてundefが格納される。もし次に、if文でスカラーコンテキストで評価されるとしたら、リストの要素数は1なので、常にifの評価はtrueになる。
2010年9月6日月曜日
Perl best practices[Perlベストプラクティス] 7章 ドキュメント
ディレクティブ≒コマンド C言語の#includeや#defineもディレクティブ
ユーザドキュメントと、テクニカルドキュメントを区別する
- ユーザドキュメント
- エンドユーザ向け
- ユーザはperldocを使って説明を読むだろう
- だからドキュメントはコードのPODのパブリックセクション(=head1,=head2,=over,=item,=backセクション)に書く
- テクニカルドキュメント
- 開発者、保守担当向け
- PODも読むが、ソースコードを読む時間のほうが長いだろう
- だからドキュメントは非パブリックセクションに追いやる
また、内容を区別すること。
PODテンプレートを用いる
まったくの白紙状態からドキュメントを書くのは、心理的にかなりキツい。
テンプレを用意して、それを埋める作業にすることで、モチベを維持する。
Emacsでの設定は、
;;load an application template in a new unattached buffer...
(defun applivation-template-pi ()
"Inserts the standard Perl application template" ; For help and info.
(interactive "*")
(switch-to-buffer "application-template-pl"))
;; Set to a specific key combination...
(global-set-key "\C-ca" 'application-template-pl)
拡張テンプレート
前述テンプレートは最小限のものであり、場合により、拡張する。
たとえば、よくある質問と回答履歴とか。
ユーザドキュメントはソースファイルに添付する
ドキュメントを別に管理するのは面倒
近接性
- ドキュメントは1ヶ所にまとめること。
- 対応するコードに書くと、コードを汚すことになる。
- なぜか対応するコードの近くにあるコメント(ドキュメント)は、更新されない傾向があるらしい。
PODはファイルの最後に記述する
- コードを読もうとして、先頭に数百行のドキュメントがあるとゲンナリする。
- __END__ラベルのあとに配置して、コンパイル時に調査されないようにする。
- __DATA__セクションを使用する場合は、ドキュメントを=pod/=cutで囲み、__DATA__ラベルの直前に配置する。
テクニカルドキュメントは適切に再分割する
構成ごとに別々の.podファイルまたはテキストドキュメントを使用する。
構成例は以下。
- テクニカルドキュメント構成
- 外部資料
- 設計書
- データディクショナリ
- アルゴリズムの概要
- 変更ログ
ユーザドキュメントの「See Also(参照)」セクションでこれらのファイルの存在を明記すること。
内部資料、実装の説明、保守に関する注意事項などは、以降の節で説明する。
コメント
- チームに適したコメントテンプレートを作成する。
- サブルーチンやメソッドの内部資料を作成するために、箇条書きできるテンプレを用意すること。
- 一貫性のあるコメントは読み易いし、ここでも「空白を埋めるだけ」効果で心理的なしきい値が低くなる。
アルゴリズムのドキュメント
- アルゴリズムのドキュメントは、ソースコード内の一行コメントとして、インラインコメントする。
※これはソースの上にかく(次節の「補足用ドキュメントはソースと同じ行の行末に添える」)
# 元の配列をキャッシュする
$raw .= $var_name;
- それらをつなげて読めばアルゴリズムが推測できるようになるのが理想。
- コメントが1行で収まらない場合は、コードを見直す必要がある。
補足のためのドキュメント
コードは読めばわかるように書いてあるはずなので、そのヒントとなるドキュメントは必要ないが、専門用語が含まれている場合などは補足しておく必要がある。
この場合は、ソース行末に続けてインラインコメントを書く。
my $QFETM_func_ref; # 量子電界効果伝導モード関数を格納する
コメントがソース行末に収まらない場合は、「表にでない」PODを使用する。後の節で説明。
防御的ドキュメント
- 迷ったものにはコメントをつける。
- 未来の自分も、他人も後から苦労するのが目にみえている。
- 例えば組み込み関数octは引数を8進数にした結果を返すのではなく、実際は、8進数を10進数に変換する。
- マニュアルで調べなければいけないもの、構文や意味を理解するのに5秒以上かかるものにはインラインコメントをつける。
@option = map +{ $_ => 1}, @flags; # マップブロックではなく
# ハッシュコンストラクタ
コメントを書くよりもコードを書き直すべきか考える
- コードにコメントを残す必要がある場合は「少なからず」コードを変更する必要性があることを意味する。
先ほどのmapを変更すると以下のようになる。
@options = map { {$_ => 1} } @flags; #ハッシュ参照を返すマップブロック
この例はコメントは要らないかもしれないが、書く場合は事実を端的に書く。
長いテクニカルドキュメントは「表にでない」PODセクションを使用する
ソースコード内に長い内部資料を埋め込むには、=forと=begin/endを使う。
以下、=forと=begin/endの違いと使い分け
- 共通項目
- コンパイル時に無視され、PODフォーマッタに処理されても目に見える出力がされない。
- =cutを指定する必要がある。
- コンパイラをドキュメントスキップモードからPerlコードコンパイルモードに戻すため。
- フォーマット名を指定する。例えば=for html,=for groffなど。
- フォーマット名がPOD標準の名前でない場合は、作成した文書がソースコード以外から参照できなくなるかもしれないが、あえてそうする場合は、フォーマット名の頭を大文字にして、最後に:を付ける。これは、設計や実装が特殊であることを示す特殊な目印になる。
- 表にでないテクニカルドキュメントはできるだけコードの近くに配置する。
- 内部で使用するものであり、PODフォーマッタの対象ではないので、PODマークアップを使用しない。
- =for形式を使用する場合
- 基本的に=for形式を使う。
- 1段落で収める
- =forは最後に空行が終了条件である点だけが=begin/endと異なる。
- =begin/end形式を使用する場合
- 複数段落
- サンプルコードの埋め込みが必要
ドキュメントを見直す
- ユーザや保守担当者との意志疎通として書かれるドキュメントは、あいまいであってはならない。
- ドキュメントを見直す場合は、書き上げたPODを読み返すのではなく、レンダリングされたものを読む
- PODをperldocを使用してテキストで読む
- pod2htmlでHTMLで変換
- pod2latexで変換
適切な表示ツールを使って最後まで読むのがいい。
- コードを良く知らない人にチェックしてもらう。
Perl best practices[Perlベストプラクティス] 8章 組み込み関数
ソート中にソートキーを再計算しない
- perlのソートはマージソートなので、sortの度にsortブロックが
回呼び出される。
以下に記載されていた方法を紹介するが、それぞれ得手不得手があるので、使うときにベンチマークをすることが重要。
(8.8節では、ソートを最適化するための方法をもう一つ紹介されている。)
一度計算した結果をハッシュに登録しておく
Orcishと呼ばれる手法(キャッシュをOrするが名前の由来)
#スクリプトのSHA-512によるソート use Digest::SHA qw( sha512 ); @sorted_scripts = do { my %sha512_of; #ハッシュを参照して、ハッシュに登録されていなければ、$sha512($a)を計算して、結果をハッシュに登録 sort { ($sha512_of{$a} ||= ($sha512($a)) cmp ($sha512_of{$b} ||= ($sha512($b)) } @scripts; };
他でも計算結果を使い回す場合は%sha512_of宣言をソートの外に出して、スコープを変える。またその場合はdoブロックは要らない。
予めダイジェストを計算しておき、ソートする
この場合、ハッシュスライスが使える!
@sha512_of{@script} = map { sha512($_) } @scripts;
ただし、ソートから例外が出されて、ソートが途中で終わる可能性があるときは、予め計算したSHA-512ダイジェストが無駄になる。
schwartzian変換
このような方法もあるが、若干低速らしい。
#sortした値自体が欲しくないときに使う。 @sorted_scripts = map { $_->[0] } #スクリプトを抽出 sort { $a->[1] cmp $b->[1] } #ダイジェストでソート map { [$_, sha512($_)] } #予め対応するスクリプトとダイジェストの結果を格納する。 @scripts;
サブルーチンの結果を記憶する
一番分かりやすいのは、サブルーチンの結果を記憶すること。(ただし前述のものよりは若干低速。schwartzianとはどうだろう・・・)
どういうことかと言うと、一度サブルーチンを呼び出すと、そのときの引数と結果を記憶していて、同じ引数で再度サブルーチンが呼び出された場合は、記憶している結果をそのまま使用するというもの。
PerlではMemoizeモジュールで簡単に実現できる。
use Digest::SHA qw( sha512 ); #SHA-512ダイジェスト関数の自己キャッシュ use Memoize; memoize('sha512'); #自動的にキャッシュされたSHA-512ダイジェストによるソート @sorted_scripts = sort { sha512($a) cmp cha512($b) } @scripts;
リストの反転にはreverseを使う
分かりやすいし、早い。
@sorted_results = sort {$b cmp $ a} @unsorted_results;
より
@sorted_results = reverse sort @unsorted_results;
とした方が早いことすらある。
また、forループを逆順にループするときに使用すると、簡潔に書ける。
for my $remaining (reverse $MIN..$MAX) { print "T minus $remaining, and counting...\n"; sleep $INTERVAL; }
これをCスタイルループのデクリメントで実現しようとすると、前にも紹介した
Cスタイルの弱点が現れてしまう。
1、比較演算子の選択を誤る(<と<=を間違えるとか)
2、反復変数の更新をしくじる
スカラーの反転にもreverse
スカラーにreverseを適用すると文字列の文字を並べかえることもできる。
ただし、スカラーであることを明記すること。
my $visible_email_address = scalar reverse $actual_email_address;
明記しないと、以下のようにサブルーチンの引数として使用した場合、
リストに作用して、ひとつしかないリスト要素を反転してしまう。
add_email_addr(reverse $email_address);#add_email_address(scalar reverse $email_address)とする。
固定幅のデータにはunpackを使用する
X123-S000001324700000199のような固定長のテキストデータの塊が使われていることがよくある。
ここから、特定文字列を取得することを考えると、
- substrはコードが汚くなるし、処理が遅い。
- 正規表現を使っても処理が遅い。
組み込み関数unpackはこのような時のために最適化された関数。'A'指定子を使用することで、文字列から文字を取り出すことができる。
Readonly my $RECORD_LAYOUT => 'A6 A10 A8'; #6文字 10文字 8文字の並びで指定 while (my $record = <$sales_data>) { my ($ident, $sales, $price)#文字列右から6文字,10文字,8文字を、リストに格納 = unpack $RECORD_LAYOUT, $record; #それぞれハッシュ登録してpush push @sales, { ident => translate_ID($ident), sales => $sales * 1000, price => $price, }; }
データがX123-S 0000013247 00000199のように、何かで区切られている場合は@指定子を使用して、
文字列の何文字目がそれぞれのフィールドの先頭なのかを教える。
Readonly my $RECORD_LAYOUT => '@0 A6 @8 A10 @20 A8' #@は昇順である必要はないので、好きな順序で文字列を取得することができる。
可変幅のデータにはsplitを使用
一般的に使用されていないが、splitの第3引数の使用を推奨する。
この第3引数はsplitから返されるフィールドの最大数を指定する。
予め返されるフィールドの数がわかっている場合は、数を指定することで、無駄な分割を防ぐことができる。
(例えば、splitの戻り値を受け取る変数が3つしかない場合、それ以上分割する必要はない)
ただし、注意するべきことは、
指定する第3引数は、取得したいフィールドの数+1を指定すること。
レコードから、「最初の3つ」のフィールドを取得するためには、レコードを4つに分割する必要がある。
複雑な可変幅のデータ
Text::CSV_XS、Text::CSV::Simple,Text::CSV
単純な文字列の並びならsplitで良いが、フォーマットルールは気まぐれに変化する。
これに対応するのは苦痛なので、モジュールを使用する。
Text::CSV_XSでは、フィールドセパレータ、エスケープ文字、フィールド引用符デリミタとして
使用される文字を指定し、フィールドから文字列を取得する。
Text::CSV::Simpleは、Text::CSV_XSのラッパーで、よりシンプルにモジュールを使用できる。
use Text::CSV::Simple; my $csv_format = Text::CSV::Simple->new({ sep_char => q{,}, #フィールドセパレータ escape_char => q{\\},#バックスラッシュのついた文字は常にデータ quote_char => q{"}, #フィールドは2重引用符で囲むことが可能 }); $csv_format->field_map( qw( ident sales price) ); #フィールドの順に名前を指定 #一度の呼び出しで、ファイル全体を読み込みハッシュリストに変換 #その後forでハッシュを使用してフィールド値を取得している。 for my $record_ref ($csv_format->read_file($sales_data)){ push @sales, { ident => translate_ID($record_ref->{ident}), sales => $record_ref->{sales} * 1000, price => $record_ref->{price}, }; }
Text::CSVはピュアPerlで実装されているので、コンパイル済みのモジュールを使用できない環境なら
こちらを使用する。
文字列の評価
eval
- 処理に時間がかかる
- コンパイル時に警告が生成されない
evalを使用したい状況の代表は、
「ユーザから渡された式に基づいて、新しいサブルーチンを作成する」こと。
匿名クロージャによるサブルーチンの作成
作成予定のサブルーチンの名前と、作成するサブルーチンの機能を選択するオプションとなる引数をサブルーチン作成用のサブルーチンに渡して、
そこで、オプションで指定した匿名サブルーチンを作成する。
そして、Sub::Installerを用いて、名前と匿名サブルーチンを呼び出し元の名前空間にインストールする。
これは構文に誤りがあればコンパイルエラーになる。
ソートの自動化
Sort::Makerによるソートルーチンの作成
いろいろなソート方法を実現できる。
引数を降順でソートするようなよく使う単純なソートも名前付き引数で指定できるので、
積極的に使うようにする。
部分文字列
以下の組み込み関数に値を代入するという奇妙な構文は、
substrで指定した部分文字列を右辺の文字列で置換する機能をもつ。
だた、これは理解しにくく、かつ処理が遅い。
substr($addr, $country_pos, $COUNTRY_LEN) = $country_name{$country_code};
perl5.6.1からはsubstrに第4引数が指定できるようになり、この引数で部分文字列の置換ができるようになった。
こちらの方が構文も自然で高速であるので、常にこちらを使用する。
ハッシュ値
ハッシュのvaluesはエイリアスなので、値を直接利用することができる。
ただし、perl5.6以前はできないので、インデックスキーを指定してアクセスする必要がある。
グロブ
入力演算(IOなど)ではない場合は、<>ではなく、グロブであることを明示するglob()を使用すること
my @files = <*.pl>;
はreadline処理と勘違いされやすいが、グロブである。
山括弧<>が入力演算の働きをするのは
- 空である場合<>
- 裸のワードだけ<DATA>
- 単純なスカラー変数<$input_file>
山括弧にそれ以外のものが含まれる場合は、ディレクトリ参照が実行される。
つまり、拡張子が*.plのファイル名のリストが返されることになる。
また、最悪なパターンとして、'*.pl'が定数として定義されてしまった場合、
スカラー変数を<>で囲むことになるので、グロブではなくなってしまう。
常に、
Readline my $FILE_PATTERN => '*.pl';
my @files = glob($FILE_PATTERN);
のようにする。
スリープ
組み込み関数sleepは整数の値しか対応していないが、selectの副作用を使ったスリープで1秒未満のスリープを実現しないこと。代わりにTime::HiRes::usleep関数を使用すること。
Time::HiRes::usleep関数を使用できない場合に、selectを使用する場合はサブルーチンでカプセル化すること。
ただし、サブルーチン呼び出しのオーバヘッドでわずかだか正確さに影響がでる。(気にしなくていい程度らしいが)
mapとgrep
mapとgrepの式は、常にブロック{}で囲むこと。そうしないと、後に続くリストと区別がつきにくくなる。
ユーティリティ 組み込みでない関数を使用する
次の3つのモジュールには便利な関数が複数存在する。
- Scalar::Util
- List::Util
- List::MoreUtils
詳しくは紹介しないけれど、first,reduce,apply,uniqなどがよく使いそう。
2010年9月5日日曜日
perlモジュールメモ
「『Test::Base』というテスト・スクリプトを書くのが便利になるモジュールがあって、そのモデルはIngyが作ったモデルなんですけど、これは発想としてすごいなあと。宮川さんが作った『Web::Scraper』というモジュールもすごい。あと、yappoさんが作った『Class::Component』も最近使い始めたんですけどすごいなと。いま挙げた3つはここ1~2年のエンジニア界の動きにかなり影響を大きく与えたものだと思います」
Web::Scraperはお世話になりました。
2010年9月4日土曜日
Perl best practices[Perlベストプラクティス] 6章 制御構造 6.16~6.22
著作権的に、ソースコードの引用しすぎかな、とも思ったけど
Perl best practicesの日本語版は、GoogleBookで読めるようだから、もーまんたいかな?
ネストしまくりのif文を使用しない
コードが読みにくいし、実行コストも高い。とにかく人間が頭に記憶していられる短期記憶はしょぼいので、読みにくいコード、長いコードは保守しにくい。
以下の節から対策案が紹介されている。
値の交換
ある値を何かに変換する作業、例えば、一から十を1から10に変換するなど、は大量のif-elseの条件文の中に正規表現を書くのではなく、参照テーブルを用いる。
my %num_for = {
'zero' => 0, '零' => 0,
'one' => 1, '一' => 1, 'une' => 1,
'two' => 2, '二' => 2, 'deux' => 2,
'three' => 3, '三' => 3, 'trois' => 3,
'four' => 4, '四' => 4, 'quatre' => 4,
'five' => 5, '五' => 5, 'cing' => 5,
);
sub words_to_num {
my ($words) = @_;
my @words = split /\s+/, words;
my $num = $EMPTY_STR;
for my $word (@words) {
my $digit = $num_for{lc $word};
if (defined $digit) {
$num .= $digit;
}
}
return $num;
}
print words_to_num('one zero eight');
変更時も参照テーブルを変更するだけで、関数には手をつけなくていい。また参照テーブルは、スカラー定数である必要はなく、サブルーチンでもいい。
なので、以下のような、引数によって呼び出すサブルーチンを変えることもできる。
package Debugging;
use Crap;
use Log::Stdlog { level => 'debug' };
my %debug_mode = {#匿名サブルーチンのテーブル
off => sub{},
logged => sub{ return print {*STDLOG} debug => @_; },
loud => sub{ carp 'DEBUG: ', @_; },
fatal => sub{ croak 'DEBUG: ', @_; },
};
sub import{
my $package = shift;
my $mode = @_ > 0 ? shift : 'loud';
my $debugger = $debug_mode{$mode};#ここでテーブルから匿名サブルーチン選択
use Sub::Installer;
caller()->reinstall_sub(debug => $debugger); #呼び出し元のPackageName(名前空間)でdebugと言う名前のサブルーチンで、中身は$debuggerに入ってる匿名サブルーチン
#reinstall_subは以下のコードと等価
# do {
# no warnings 'redefine';
# no strict 'refs';
# *{$PackageName.'::'.$subname} = $sub_ref;
# }
return;
}
これで、デバッグの種類が増えても、テーブルに追加するだけで、対応できる。表形式の3項演算子
my $A = $B ? $BB
: $C ? $CC
: $D ? $DD
;
利点は、- 代入文が一つ
- テーブルのように見える
- else文が絶対に必要
do-whileを使用しない
do-whileループではnext,last,redoは使用できない。しかも記述しても無視されるだけなのが、また厄介。
nextなどは、ラベルのついたループを検索して反復するが、do-whileはループではない。
do-whileはポストフィックス(接尾辞)の変更されたdoブロック。
do-whileのように必ず1回だけは処理をして欲しい場合は、
処理の最後にフラグを立てて($flag=1;)、ループ条件に!$flagを入れておく。
もしくは、while(1)の無限ループの最後に「return ループ終了条件サブルーチン;」を入れる。
データのチェックは早い段階で行う
すべてのデータが揃ってから、if文などで評価するのではなく、一つ一つでのデータが得られるごとに評価して、不適切なら、処理を抜けるようにしておくと、効率がいい。
if文のネストも深くならない。
制御を一元化するためにループ構造をねじ曲げない
returnする箇所は1ヶ所にまとめるべきだという主張があるが、これは結局は複数箇所にフラグを用意したり、処理を中断する処理が随所に散らばっているため、returnを1ヶ所にすると、制御が一元化されて、ミスしにくいというのは錯覚。コードも見にくくなるので、先ほどのデータチェックのとおり、その場で処理を抜けるようにする。
#return1箇所ver
while ( !eof
&& $tries < $MAX_TRIES
&& ( $int !~ $INTEGER || $int < $MIN_BIG_INT )
) {
print 'Enter a big integer: ';
$int = <>;
if (defined $inf ) {
chomp $int;
if ($int eq $EMPTY_STR) {
$int = 0;
$tries--;
}
}
else {
$eof = 1;
}
tries++;
}
#ひとつずつ評価ver
INPUT:
for my $attempt (1..$MAX_TRIES) {
print 'Enter a big integer: ';
$int = <>;
last INPUT if not defined $int;
redo INPUT if $int eq "\n";
next INPUT if $int !~ $INTEGER;
chomp $int;
last INPUT if $int >= $MIN_BIG_INT;
}
whileとforの使い分け
- for
- 固定回数ループのとき
- 反復カウントや、条件式が1つのとき
- 反復カウントがインクリメントのとき
- while
- 不規則にカウントされるもの
- 反復カウント変数や、条件式が2つ以上のとき
- 反復カウントがデクリメントされるとき
forを使用することで、固定回数ループであることを読み手に主張できる。
また、(Cスタイルでないforだと)カウント変数が不要になり、カウント変数の評価を間違えたり(<を<=にしたり)、カウント変数のインクリメントを忘れることがない。
固定回数ループといっても、例外は欲しい。この場合はredoを使うことで、反復回数を更新することなく、forの頭に処理を戻すことができる。
next,last,redoを使う場合はラベルを付ける。
- 途中で制御フローが変化することを知らせることができる
- コードが追加されたとき、バグが入りにくい。バグがでても見つけ易い
2010年9月3日金曜日
Perl best practices[Perlベストプラクティス] 6章 制御構造 6.1~6.15
ポストフィックス形式のif文は制御フロー文にのみ使用
LOOP:
for my $mem (@mems) {
last LOOP if $mem f < 0;
}
next,last,redo,return,goto,die,croak,throwキーワードは、目立つ左端にくるように、if文を後ろにもってくる。
ポストフィックス形式のunless,for,while,untillを使用しない
- 特にfor文は、ポストフィックス形式は使わない。
- 反復子変数に$_を使う必要がある。
- 評価式を簡単にはネストできない
- 後からfor文が肥大化すると、ポストフィックス形式は無理がある。初めから使わない。
否定制御文(unless,untill)を使用しない
- 拡張が難しい
- 2重否定を理解するのが難しい
- 否定制御文(unlessf)を否定演算子(!)に置き換えた場合、以降の条件をすべて反転させないといけない
- もし否定制御文を使用する場合は、コードが見やすくなる場合だけにする
- このとき、否定制御文の条件式は、肯定的なものにすること。
Cスタイルのループを使用しない
- C言語のループは素直に読みにくい
以下のCスタイルのループは、読み手がロジックを解明する必要があるが、2番目のループは、やりたいことがそのまま書いてある。
for (my $n=4; $n<=$MAX; $n+=2) {
print $result[$n];
}
RESULT:
for my $n (4..$MAX) {
next RESULT if odd($n);#ベンチマークでパフォーマンスに問題ありならn%2を使用
print $result[$n];
}
また反復カウント用変数や条件式が2つ以上ある場合、または、反復カウンタ変数が線形に変化しない場合はwhile文を使う。(139ページ参照)
values関数から得られるリストは、エイリアスのリストである
エイリアスなので、実際のハッシュ値を書き換える。
ただし、delete関数だけは値のエイリアスでは不十分で、キーも知る必要がある。
この場合は、5章で解説されたハッシュスライスを使う。
my @unacceptable_words
= grep {$translation_for{$_} =~ m/ $PROFANITY /xms}
keys %translation_for;
delete @translation_for{@unacceptable_words};#スライスでdelete
反復にインデックスと値の両方が必要なときは、エイリアス化する
use Data::Alias;
for my $agent_num (0..$#operatives) {
alias my $agent = $operatives[$agent_num];
if($on_disavowed_list{$agent}) {
$agent = '[DISAVOWED]'; #エイリアス化してないと、ここでインデックスが必要になる
}
ハッシュの場合も同じ手法を用いる。
しかしeach関数はこの手法は仕えない。each関数で返される値は、ハッシュ値のエイリアスではなくコピーである。
非レキシカルループ反復子
my $client;
SEARCH:
for $client (@clients) { #forスコープ
last SEARCH if $client->holding();
}
if ($client) {#forの反復カウンタは常に破棄されるので、取得は不可能
$client->resume_conversation();
}
for文の$clientは、forのスコープで新しくこっそり作られたレキシカル変数であり、その前の$clientとは関係ない。ここで、for my $clientとしていれば、読み手に分かりやすく伝えることができる。(もちろん違う名前のほうがいい)
リストからリストの生成するには、mapを使用する
ある配列の要素を2乗して新しい配列に、反復で一つずつpushするコードでは、pushする度にメモリの再割当てが必要になる。
しかしmapは、事前に必要な要素数を把握する。(1つの要素から複数の要素を生成する場合は別)
またmapは、高速なCコードで処理される。
リスト値の検索はmapかfirstを使う
forの代わりに使うと、理解しやすい効率的なコードになる。
変換先と変換元の配列が同じ場合はmapではなくforを使う
mapは追加メモリを割り当てたあとに、一時的に保持していたリストを代入するが、forは変換元リストの既存のメモリを再利用できる。
複雑なマッピング
複雑な反復にコードが肥大化したときは、関数に避けて、単純なmap式から呼び出す。
リスト関数で$_を変更しない
$_はエイリアスなので、もとの値が変更されてしまう。分かりにくいコードになる。
2010年9月2日木曜日
Perl best practices[Perlベストプラクティス] 5章 変数 5.8~5.12
$_はエイリアス
$_はエイリアス(別名)なので、元の値を変更してしまう。
想定外の動作をする可能性があるので、常に変数に代入すること。
for,map,grepなどで問題が起こり易い。
my $string = (@_ > 0) ? shift : $_; #引数ひとつだけ
for my $orig_arg (@_ ? @_ : $_ ) #リスト作成
配列の最後の要素からアクセスする場合は、負のインデックスを使用する。
利点1:短くて読み易い
#最後から3番目
$frame[@frames-3]
$frame[$#frames-2] = 1;
$frame[-3] = 1;
利点2:配列の要素数を越えたアクセスに対して例外を受け取ることができる
要素数が2つの配列に対して、frame[@frames-3]とすると、これは一周して最後の要素を指してしまう。
frame[-3]なら例外が発生する。
スライス 要素へのアクセスの省略構文
@ary[-1,-2,-3]
= @a{'top', 'prev', 'backup'};
@ary[-1,-2,-3] は ($ary[-1], $ary[-2], $ary[-3])と同じ。
ハッシュへのアクセスも同じ。
$を@にして、必要なだけキーを追加する。
@hash{'top','prev','backup'}
負のリスト範囲に注意
@frames[-1..-3]のように最初の値の方が大きい数字から範囲指定すると空のリストが生成される。
これは@frames[-3..-1]としなければならない。
表形式のデータ構造を利用したスライス
データが多くなってきた場合、
Readonly my %CORRESPONDING => {
'top' => -1,
'prev' => -2,
'buckup' => -3,
'spare' => -4,
'defauly' => -5,
};
@frames[ value %CORRESPONDING ]
= @active{ keys %CORRESPONDING };
利点は、
- データが増えてもハッシュスライスのコードは変更しなくていい。
- ハッシュなので、ハッシュ表の要素の順番は何でもいい。
これが上手くいくのは、valuesとkyesが常に同じ順序で、ハッシュエントリを走査するため。
単純な配列のテーブルを使用すると良い場合もある。
#@STAT_FIELDSの要素は、stat関数から返されるリストの順番
Readonly my @STAT_FIELDS
=> qw (dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks );
sub status_for {
my ($file) = @_;
my %stat_hash = ( file => $file);
@stat_hash{@STAT_FIELDS} = stat $file; #ファイル詳細情報をリストで返す
return \%stat_hash;
}
warn 'File was last modified at ', status_for($file)->{mtime};
2010年9月1日水曜日
Perl best practices[Perlベストプラクティス] 5章 変数 5.1~5.7
可能な限りレキシカル変数(my)のみを使用する
コードの独立性が下がるから。
レキシカルでない組み込み変数で避けられないものは
$_, @ARGV, $AUTOLOAD, $a, $b
など。
しかし他のほとんどの組み込み変数は必要なく、代案が存在する。
パッケージ変数を使用しない
### mainパッケージ
# ・パッケージの宣言が行われない場合、ourで宣言されたパッケージ変数は、
# mainパッケージに属する。
# ・Perlには、グローバル変数は、存在せず、パッケージ変数と
# レキシカル変数しか存在しない。
print "パッケージ宣言のない場合はmainパッケージに属する\n";
- パッケージ変数を使用すると、モジュールの内部状態を変更されるかもしれない。
- 一般的に、モジュールのインタフェースで変数を使用するのは、悪いプラクティス(17章参照)
ローカル化
ローカル化によって、パッケージ変数の内容を一時的に変更することができる。
現在のスコープを抜けると、元の値に戻る。
ローカル化したときは、前のパッケージ変数の値ではなくundefが入る。
混乱しないように、初期値は明示的に代入しておく。
有名ではない句読点変数にはuse Englishで名前を付ける。
名前を付けなければ、滅多に使用されないコードなため、コメントを信用して、
内容チェックをスルーするかもしれない。
句読点変数のローカル化
IO処理において、句読点変数の値を変更する必要がある場合がある。
その場合は、無駄に影響を広げないためにローカル化する。
正規表現のマッチ変数を使用しない
use English qw( -no_match_vars );
で3つのマッチ引数が作成されないようにする。
- $PREMATCH (または $`)
- $MATCH (または $&)
- $POSTMATCH (または $')
マッチ変数は、正規表現が成功すると、追加情報として保存される。
マッチ変数はグローバルなので、どこかでマッチ変数が使用されていると、すべての正規表現のマッチが成功する度に、値が書き換えられる。このため、実行の効率が下がる。
また、自分がいざマッチ変数を参照するときに、中身が「どこの正規表現」で得られた結果なのか不明になる。異なるファイルにある正規表現である可能性すらある。
代わりにRegexp::MatchContextを使用する。
マッチ変数の代わりに、PREMATCH(),MATCH(),POSTMATCH()というサブルーチンが追加され、呼び出すことで、プレマッチ部、マッチ部、ポストマッチ部を得られる。
このとき、上記の値が変更される条件は、
(?p)というマーカが付いている正規表現かつ、マッチが成功したときではなく、マッチ変数が実際に使用されるとき。
このため、どこをデバックすれば良いかわかりやすいし、効率も良くなる。
use Regexp::MatchContext;
my ($name, $birth_year)
= $manuscript =~ m/(?p) (\S+) \s+ was \s+ born \s+ is \s+ (\d{4})/xms;
if ($name) {
print PREMATCH(),
qq{<born date="$birth_year" name="$name">},
MATCH(),
q{</born>},
POSTMATCH();
}
さらに、読み込み専用ではなく、substr形式の文字列が返されるので、MATCH()に代入すると、元の文字列が変更できる。
なので、正規表現による置換をわかりやすく記述することができる。
use Regexp::MatchContext;
if($html =~ m{(?p) <body> .* </body>}xms) {
PREMATCH() = $STD_HEADER; #標準ヘッダを<body>の前に設置
MATCH() = verify_body( MATCH() ); #コンテンツ
POSTMATCH()= '</html>'; #</body>の後ろは</html>だけにする
}
Perl best practices[Perlベストプラクティス] 4章 値と式
文字列の変数展開 文字列デリミタ4つのルール
- 変数を展開する場合は、2重引用符。
- 展開しない場合は、単一引用符。
- 単一引用符を文字列として扱いたい場合は、q{}を使用する。
- {を文字列として扱いたい場合は、q[]とする。
関連する文字列が並んでいる場合は、読み難いので、一つのルールで統一する。
my $title = 'Perl Best Practices';
my $publisher = q{O'Reilly};
my $end_of_block = '}';
my $closing_delim = q['}];
my $citation = "$title ($publisher)";
#以下に修正
my $title = q[Perl Best Practices];
my $publisher = q[O'Reilly];
my $end_of_block = q[}];
my $closing_delim = q['}];
my $citation = qq[$title ($publisher)];
空の文字列に"",''を使用しない
以下を使用のこと。
$error_msg = q{};
1文字の文字列
スペースやタブは
$a = q{ };
$b = "\t";
とすること。
リテラルの引用符も美しくないので、q{}を推奨。
名前付きエスケープを使用する。
Perlが表現できない文字は、数値のエスケープ(バックスラッシュとそれに続くASCII値を””で囲んだもの)で表せるが、use charnamesプラグマを用いて、名前付きエスケープを使用すること。
use charnames qw( :full);
$escape_seq = "\N{DELETE}\N{ACKNOWLEDGE}\N{CANCEL}Z"; # "\177\006\030z"
マジックナンバーはReadonlyで定数化する(use constantを使用しない)
定数はすべて大文字にし、定数名と値は=>(ファットコンマ)で見やすくする。
use Readonly;
Readonly my $BOOK_NUMBER => 42;
空文字q{ }を定数化すると便利。
なぜconstantを使用しないのか
use constantで作成されるのは裸のワードなので、定数が展開されない。
以下はReadonlyを使用した場合。
$msg = <<"END_MSG";
$BOOK_NUMBER
END_MSG
$title{$BOOK_NUMBER}
もっとも重要なことは、
レキシカルスコープをもつ定数を実行時に生成できること。
EVENT:
while (1) {
use Readonly;
Readonly my $EVENT => get_next_event();
last $EVENT if not defined $EVENT;
print "$EVENT\n";
}
8進数は組み込み関数octを使用する。
0で始まる整数で8進数を表現しない。
長い数字は桁区切りする
Perl5.8以降では、数字の間にアンダーバーを挿入することができる。(前versionは3桁区切りのみ)
$US_GDP = 10_990_000_000_000;
複数行の文字列は、\nで明示的に改行する
$usage = "Usage: aaaaa\n"
."bbbbb\n"
;
この\nを書かないで、
$usage = "Usage: aaaaa
bbbbb";
と文字列を折り返しても暗黙的に改行されるが、読みにくい。さらにインデントが左端による。
文字列が3行以上ならヒアドキュメントを使用する。
$Usage =<<"END_USAGE";
すきな文字列記述
END_USAGE
ヒアドキュメントのインデント問題
ヒアドキュメントはインデントが左端になってしまう。
定義済み変数かサブルーチン(ゼアドキュメント)にファクタリングする。
use Readonly
Readonly my $USAGE => <<'END_USAGE';
好きな文字列
END_USAGE
実行時に値を決定したい場合はサブルーチンを使う。
sub build_usage{
my($prog_name, $file_name) = @_;
return <<"END_USAGE";
$prog_name $file_name
END_USAGE
}
ヒアドキュメントのターミネータ
ラベルを見つけやすくするように、頭にENDを付ける。
ヒアドキュメントの引用符
- <<のあとのラベルを単一引用符で囲むと、変数は展開されない
- 2重引用符で囲むと変数が展開される
- 引用符を使わない裸のラベルでは、2重引用符と同じ結果が得られるが、使用しないこと
裸のワードを使用しない。
use strict qw( subs )で使用を禁止する。
裸のワードは引数なしのサブルーチンと名前が被る。
ファットコンマ
- ハッシュを生成する際に、=>を使用すれば、キーを引用符で囲む必要がない。
- =>の外観は、値の移動や変化の印象を与え易いが、動作の解釈に誤解を与えることもあり、ハッシュエントリや名前付き引数、その他の名前と値の組、のときのみ使用したほうが混乱しない。
文の順序付けはコンマではなく、doブロックを使う
- スカラーコンテキストでは、コンマは順序付け
- リストコンテキストでは、コンマはセパレータ
- 役割が2つあるのは混乱するので、順序付けにはdoブロックを使う。
C言語のforループのように、
for ($min=0, $max=$#samples, $found_target=0; $min<=$max) {
#略
}
と書いた場合、C言語と同じく、コンマは弱いセミコロンのような働きをし、順序付けを行う。
これはdoブロックで以下のようになる。
for (do{$min=0; $max=$#samples; $found_target=0;}; $min<=$max) {
#略
}
この場合は、そもそも初期値を指定しているdoブロックの中身をforの前に書いておく方がもっと良い。
優先度の高いブールと低いブールを混在させない
- 優先度の低いand,notはいっさい使用しない。
- orだけは、組み込み関数の代案を指定するために使用する。(or croak "...";など)
next CLIENT if not $finished || $result < $MIN_ACCEPTABLE; #これは以下のどれになるか
next CLIENT if (not $finished) || ($result < $MIN_ACCEPTABLE);#期待しているのはコレ
next CLIENT if not ($finished || $result < $MIN_ACCEPTABLE);#実際はコレ
リストは括弧で囲む
コンマは代入より優先順位が低いので、リストは常に括弧で囲む。
リストのメンバの評価には、any()を使用し、文字列リストの場合は、テーブル参照を利用
List::MoreUtilsモジュールのany()関数はgrepとほぼ同じだが、
値のどれかが評価ブロックをパスした時点で、trueの値を返して終了する。このため、grepよりも効率的。(パスしない場合は、いずれも値もfalseを返す。)
(でもfirstとanyは何が違う?)
これは、コードブロックに任意の等価評価を作成できるため。
if( any { $fugitive->also_known_as{$_} ) @guests ) {
#略
}
リストのメンバ評価でeqを使用する場合は、参照テーブルを使用するほうがはるかに効果的。
ハッシュアクセスは、配列を線形アクセスするよりも高速である(線形探索を短絡できる場合も含めて)し、コードも読み易い。
Readonly my %IS_EXIT_WORD
= map { $_ => 1 } qw(
q quit bye exit stop done last finish aurevoir
);
if( $IS_EXIT_WORD{$cmd} ) {
abort_run();
}