高度なリスト操作 map

次は grep とよく似ている map を勉強します。
map も grep と同じく、第一引数に「式」もしくは「ブロック」をとり、第二引数に「リスト」をとります。そして、やはり grep と同じように、渡したリストの各要素を $_ にセットしながら、第一引数の式・ブロックを評価していきます。
では、何が違うのかというと、grep は与えられたリストの要素を「選別」したリストを返す関数でしたが、map は与えられたリストから「別のリストを作って」そのリストを返す関数です。第一引数で渡した式・ブロックを呼び出して返ってきた値をつなげて、新しいリストを作ります。

use strict;
 
my @nums = (1..10);  # 1から10のリスト
print "@nums\n";
 
my @squares = map $_ * $_, @nums;
print "@squares\n";

これを実行すると

1 2 3 4 5 6 7 8 9 10
1 4 9 16 25 36 49 64 81 100

と表示されます。@nums には範囲構文を利用して 1 から 10 の数字のリストを代入しています。
この @nums に対して map 関数を、第一引数に「$_ * $_」($_ の2乗)という式を渡して呼び出しています。@nums の各要素($_)に対してこの式($_ * $_)が評価されます。この評価した結果が順番に入った新しいリストが map の返り値となります。
このコードの場合は、@nums の各要素を2乗したリストが返ってきているわけですね。これは便利です。(^_^)


また、1つの要素に対して複数の要素を返す事もできます。

use strict;

my @nums = (1..10);
print "@nums\n";

my @twicehalf = map { $_ * 2, $_ / 2 } @nums;
print "@twicehalf\n";

今度は map の引数にブロックを渡しています。ブロックを渡す時は「,」(カンマ)は要らないんでしたね。*1
ブロックの中身は $_ に 2 を掛けたものと、$_ を 2 で割ったものをカンマで区切っています。この場合、ブロックの返り値はこの2つの値のリストになります。
実行結果は以下の通りです。

1 2 3 4 5 6 7 8 9 10
2 0.5 4 1 6 1.5 8 2 10 2.5 12 3 14 3.5 16 4 18 4.5 20 5

1つの要素に対して2つの要素を返したので、要素の数が2倍になっていますね。面白いです。
このコードを for で書くとこんな感じでしょうか。

use strict;
 
my @nums = (1..10);
print "@nums\n";
 
my @twicehalf = ();
for (@nums) {
  push @twicehalf, $_ * 2, $_ / 2;
}
print "@twicehalf\n";

こちらでも良いですね。map に比べると長いですが、わかりやすいです。一長一短でしょうか。map を使った方が「よりPerlらしい」コードになるそうです。なるほど。(^_^)


注意しなくてはいけないのが、「$_」はあくまで map に渡した配列の要素への「エイリアス」(別名)である、という事です。$_ の値を変えると渡した配列の要素の値も変わってしまいます。

use strict;
 
my @letters = ('a'..'g');
print "@letters\n\n";
 
my @words1 = map $_ x 3, @letters;
print "@words1\n";
print "@letters\n\n";
 
my @words2 = map $_ x= 3, @letters;
print "@words2\n";
print "@letters\n\n";

@words1 に代入している最初の map では、「$_ x 3」が式です。「x」は文字列を繰り返す為の演算子で、この場合「$_ . $_ . $_」と同じ意味です。map を行なった後の @letters を表示しています。
次の @words2 に代入している map では、「$_ x= 3」が式です。一見同じですが「x=」は繰り返した文字列を左辺に代入します。「$_ = $_ x 3」と同じ意味です。つまり、「$_」を変更しています。こちらでも map を行なった後の @letters を表示しています。
実行結果は以下の通りです。

a b c d e f g

aaa bbb ccc ddd eee fff ggg
a b c d e f g

aaa bbb ccc ddd eee fff ggg
aaa bbb ccc ddd eee fff ggg

最初の map の後では @letters は変化していませんが、2番目の map の後では @letters が変化しています。「$_」を変更した事によるものです。
ラクダ本によると、この機能は「公式にサポートされている機能」とのことですが、このように元の配列を変更する場合は for を使った方がわかりやすい気がします。あくまで map は「リストから新しいリストを作成する関数」なので、返ってきたリストを利用しない場合は for の方が良いですよね。
ちなみに $_ が「エイリアス」なのは grep も同じです。また、サブルーチンを呼び出した時の「@_」に入っている要素も、それぞれが呼び出しに使われた引数へのエイリアスになっているそうです。(つまり「参照渡し」なんですね(^_^) )
「$_」「@_」を直接操作するのは危険なので、なるべく他の変数にコピーしてから使った方が良さそうですね。

*1:実は最初間違えて入れてしまいました(笑) 注意しなくちゃいけませんね。