Perl の map と grep を使う

すしを奢らなければいけないなんて、バトンを渡されてから知りました。おいしい寿司が食いたい sekimura です。

今回は使いこなすと気持ちよくて、使いすぎると気持ち悪いと言われてしまう grepmap の使い方について紹介します。この二つは文法がよく似ていて、同時に使われることも多いので一気に両方の使い方を覚えるのをおすすめします。

grep: 配列をフィルターする

まずは、前回覚えた 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') が入る。

map: 配列の要素を変換する

例によって 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 = ('katsuo@example.com', 'wakame@example.com', 'tara@example.com');
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 さんお願いします。