ソート中にソートキーを再計算しない
- perlのソートはマージソートなので、sortの度にsortブロックが
回呼び出される。
以下に記載されていた方法を紹介するが、それぞれ得手不得手があるので、使うときにベンチマークをすることが重要。
(8.8節では、ソートを最適化するための方法をもう一つ紹介されている。)
一度計算した結果をハッシュに登録しておく
Orcishと呼ばれる手法(キャッシュをOrするが名前の由来)
use Digest::SHA qw( sha512 );
@sorted_scripts
= do {
my %sha512_of;
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変換
このような方法もあるが、若干低速らしい。
@sorted_scripts
= map { $_->[0] }
sort { $a->[1] cmp $b->[1] }
map { [$_, sha512($_)] }
@scripts;
サブルーチンの結果を記憶する
一番分かりやすいのは、サブルーチンの結果を記憶すること。(ただし前述のものよりは若干低速。schwartzianとはどうだろう・・・)
どういうことかと言うと、一度サブルーチンを呼び出すと、そのときの引数と結果を記憶していて、同じ引数で再度サブルーチンが呼び出された場合は、記憶している結果をそのまま使用するというもの。
PerlではMemoizeモジュールで簡単に実現できる。
use Digest::SHA qw( sha512 );
use Memoize;
memoize('sha512');
@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);
固定幅のデータにはunpackを使用する
X123-S000001324700000199のような固定長のテキストデータの塊が使われていることがよくある。
ここから、特定文字列を取得することを考えると、
- substrはコードが汚くなるし、処理が遅い。
- 正規表現を使っても処理が遅い。
組み込み関数unpackはこのような時のために最適化された関数。'A'指定子を使用することで、文字列から文字を取り出すことができる。
Readonly my $RECORD_LAYOUT => 'A6 A10 A8';
while (my $record = <$sales_data>) {
my ($ident, $sales, $price)
= unpack $RECORD_LAYOUT, $record;
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{"},
});
$csv_format->field_map( qw( ident sales price) );
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などがよく使いそう。