すしを奢らなければいけないなんて、バトンを渡されてから知りました。おいしい寿司が食いたい sekimura です。
今回は使いこなすと気持ちよくて、使いすぎると気持ち悪いと言われてしまう grep
と map
の使い方について紹介します。この二つは文法がよく似ていて、同時に使われることも多いので一気に両方の使い方を覚えるのをおすすめします。
まずは、前回覚えた perldoc を使って grep
とはなにかを調べてみましょう。
$ perldoc -f grep
grep BLOCK LIST
grep EXPR,LIST
This is similar in spirit to, but not the same as, grep(1) and
its relatives. In particular, it is not limited to using
regular expressions.
Evaluates the BLOCK or EXPR for each element of LIST (locally
setting $_ to each element) and returns the list value
consisting of those elements for which the expression evaluated
to true. In scalar context, returns the number of times the
expression was true.
「UNIX コマンドの grep
等のコマンドと似てるけど違うもの。だって正規表現以外も使えるんだぜ」とか書いていますね。LIST の全要素を($_
を局所的にセットしながら)BLOCK か EXPR で評価し、その結果が真となるものだけからなる配列を返します。スカラコンテキストの場合は、結果が真となる要素の数を返します。例えば %ENV
のキーからなる配列から "H"
で始まるものだけを抜き出すには以下のようにします。
$ perl -e 'print join " ", (grep /^H/, keys %ENV), "\n"'
HOME HISTCONTROL
grep
は BLOCK を使った場合にもっと楽しくなります。例えば、以下のように、ある二つの配列をつなぎ合わせたものから、重複を取り除いた配列を得ることができます。
my @cities = ('Sapporo', 'Nishitokyo', 'Yokohama');
my @prefs = ('Hokkaido', 'Tokyo', 'Yokohama');
my %seen;
my @uniq = grep { ++$seen{$_} < 2 } (@cities, @prefs);
## @uniq には ('Sapporo', 'Nishitokyo', 'Yokohama', 'Hokkaido', 'Tokyo') が入る。
逆に重複したものだけ抜き出したいときには以下のように grep
で取得した配列に対して grep
することで得られます。
my @lunch = ('Bento', 'Ramen', 'Onigiri', 'Curry');
my @dinner = ('Tonkatsu', 'Ramen', 'Curry');
my %seen;
my @dup = grep { $seen{$_} >= 2 } grep { ++$seen{$_} > 1 } (@luch, @dinner);
## @dup には ('Ramen', 'Curry') が入る。
例によって perldoc -f map
しましょう。
$ perldoc -f map
map BLOCK LIST
map EXPR,LIST
Evaluates the BLOCK or EXPR for each element of LIST (locally
setting $_ to each element) and returns the list value composed
of the results of each such evaluation. In scalar context,
returns the total number of elements so generated. Evaluates
BLOCK or EXPR in list context, so each element of LIST may
produce zero, one, or more elements in the returned value.
ほとんど同じことが書いてありますね。grep
はフィルターなので、得られる配列は与えられた配列のサブセットになるのに対して、map
では与えられた各要素を変換し、その結果を配列として得ることが可能です。
my %price_map = (
'Ramen' => 400,
'Curry' => 650,
'Katsudon' => 600,
);
my @today = ('Ramen', 'Curry');
my @meshi_dai = map { $price_map{$_} } @today;
## @meshi_dai には ('400', '650') が入る。
my @zei_komi = map { $_ x 1.05 } @meshi_dai;
## @zei_komi には ('420', '682.5') が入る。
grep
のときにはスルーしましたが、 BLOCK 内での $_
は元の要素のリファレンスなので、$_
を変更してしまうと、元の要素も変更されてしまいます。よく、「破壊的」と呼ばれるケースですね。これを防ぐには、 BLOCK の内部で my
変数にコピーしてから変更を加えていきます。
my @addresses = ('[email protected]', '[email protected]', '[email protected]');
my @no_spam = map { my $email = $_; $email =~ s/\@/ at /; $email } @addresses;
## @no_spam には ('katsuo at example.com', 'wakame at example.com', 'tara at example.com) が入る。
このようにして、@addresses
の要素を変更すること無く @no_spam
という変換後の要素を持つ配列を得ることができます。
grep
, map
両方共 for
, foreach
のループで書き換えることができますが、それぞれ「フィルター」と「変換」という意味をコードを読む人に的確に伝えることができるのがメリットではないでしょうか。その他にも、デバッガーやワンライナーでループ処理を簡素に書けるのも利点です。 sort
等のコマンドと組み合わせて UNIX のパイプのようにデータを処理すると自分が偉くなったような錯覚に陥るのがオススメどころです。
ただし、 grep
, map
を単にループ処理をするために、左辺値を受け取らずに使うのはコードを読む人を混乱させるので避けた方がいいでしょう。 (SEE ALSO perldoc perlstyle
)
次は nipotan さん と思ったら風邪引いてピンチだそうで。 antipop さんお願いします。