PHPのarray系関数について

ChainZ(クリエイター)
いろいろやってます。

最近node.jsを使うことが多いので、JavaScriptを書くことも多くなりました。JavaScriptって結構格好いいコードが書けます。例えば:

1
2
const children = data.map(toPerson).filter(underAge(18));
// Personの配列から未成年者を抽出する処理

どうですか?わかりやすいでしょう?こういった処理は結構最近流行りの関数型プログラミングでよく見られます。

node.jsを使い始めると、自然にforEach, map, filterを使う習慣を身につけてしまいます。もちろん、JavaScriptにもfor文、while文がありますが、それがnode.jsの中では非同期なので、使い勝手がイマイチです。forEach, map, filter同期処理なので、そういった問題はありません。

じゃあ、PHPはどうだろう?

実はPHPにも似たような関数があります:

  • array_map
  • array_filter
  • array_reduce

この三つの関数をうまく使えば、結構エレガントなコードが書けます。サンプルを作って説明します:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php

// Personクラス
class Person {
public $age;
public $name;
public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
}

// array_filter用関数、指定した年齢より下のPersonを抽出
function underAge($age) {
// Closureを返す、この関数は実際にfilterの関数として使われます
return function(Person $p) use ($age) {
return $p->age < $age;
};
}

// intの配列をPersonに変換する関数、
// (intをPersonに変換ってわけからんですが、あくまでサンプルです)
function intToPerson($i) {
return new Person("persion-{$i}", rand(1, 60));
}

// array_reduceに使う、Person配列のageをcountする用関数
function countAge($total, Person $p) {
return $total + $p->age;
}

// array_reduceに使う、Person配列をプリントする関数
function ageList($ret, Person $p) {
return $ret . "\n" . "{$p->name}: {$p->age}";
}

$children = array_filter(array_map('intToPerson', range(0, 100)), underAge(18));
/**
* 上記の処理を分解すると:
* $data = array_map('intToPerson', range(0, 100);
* $children = array_filter($data, underAge(18));
*/

$totalAge = array_reduce($children, 'countAge');

var_dump(array_reduce($children, 'ageList'));
/* (例)
persion-1: 17
persion-2: 12
persion-5: 1
persion-6: 13
...
*/

var_dump($totalAge);
/*
(例) 320
*/

どうですか?配列処理の関数をしっかり定義すれば、そのあとのコードがすごいシンプルでわかりやくになったでしょう?実はarray_*系関数を使うメリットそれだけじゃない:

  • array_*の戻り値は全てcopyなので、元データが修正されることはない。
  • Interfaceと合わせて使えば、汎用的な関数が作れるので、再利用も可能。
  • 処理が一箇所にまとめられるので、不具合の修正は一箇所で解決できる。

array_*簡単まとめ

array_map

array_mapは基本的に配列要素の変換に使います。 => という感覚で使います。第一引数は要素を処理する関数で、第二引数は処理対象となる配列になります。例えば:

1
2
3
4
5
$numbers = [1, 2, 3, 4, 5];
$doubledNumbers = array_map(function($n){
return $n * 2;
}, $numbers);
// [2, 4, 6, 8, 10];

array_filter

名前通りに、配列のフィルタリングに使います。第一引数は元の配列で、第二引数はフィルター関数です。例:

1
2
3
4
5
$numbers = [1, 2, 3, 4, 5];
$under3 = array_filter($numbers, function($n){
return $n < 3;
});
// [1, 2]

array_reduce

array_reduceは配列をまとめるに使う関数です。統計や辞書型配列に変換する時に役立ちます。第一引数は元の配列、第二引数は要素の処理関数で、第三引数はオプションで、デフォルトの戻り値です。第一引数の関数に渡される引数が二つあります:前の処理の戻り値と現在処理しようとする要素です。例:

1
2
3
4
5
6
7
8
9
10
11
12
$numbers = [1, 2, 3, 4, 5];
$total = array_reduce($numbers, function($t, $n){
// $t最初は第3引数の0, 以降はこの関数の戻り値
return $t + $n;
}, 0);
// 15

// 上記の処理をforeachでやるとすると:
$total = 0;
foreach ($numbers as $n) {
$total += $n;
}

最後に

foreachより個人的にはarray_*を使う傾向になりましたが、foreachはパフォーマンスにおいてはまだ一番なので、パフォーマンスにこだわるならまだforeachを使ったほうがいいかもしれません。しかし、差は本当にわずかなので、よりエレガントなarray_*を使ってみるのはいかがでしょう?

foreacharray_*のパフォーマンスについては下記のリンクを参考してください http://stackoverflow.com/questions/18144782/performance-of-foreach-array-map-with-lambda-and-array-map-with-static-function

もちろん、プロジェクトのコーディングポリシーというものもありますが、これからできればarray_*を積極的に使いたいと思っています。