PHPのarray_map()
とarray_walk()
は引数順序の一貫性のなさが槍玉に上げられがちなので、せっかくなのでforeach
を含めて、まとめて解説します。
はじめに三行で
- まあ
foreach
で書くのが簡単じゃね -
array_walk_recursive()
でforeach
には手の届かないことができるかもね - tadsanのpixivFANBOXもよろしく
早見表
| 関数/文 | 操作対象 | 特徴 |
|--------------------------+------------------------------------------+--------------------------------------------------------------------------|
| array_map()
| 複数の配列 | 要素にコールバック関数を適用した結果をまとめた配列を返す |
| array_walk()
| 一つの配列変数のリファレンスの要素 | コールバック関数から要素ごとのリファレンスを直接操作することができる |
| array_walk_recursive()
| 一つの配列変数のリファレンスの子要素 | 配列に含まれる配列の子要素も含めてリファレンスを直接操作することができる |
| foreach
| 一つのイテレータまたは配列(または配列の要素のリファレンス) | 配列以外のイテレータも走査できる。文なので結果は返さないが、配列のキーまたはリファレンスを使って要素を変更することができる |
今回は細かく説明しませんが、「配列」と「配列変数のリファレンス」には微妙な差があります。
array_map()
とarray_walk()
の差がわかりにくいケース
この例を見ると、array_map()
, array_walk()
, foreach
のどれを使っても大差ないように見えます。
<?php
$ss = ["apple", "banana", "orange"];
// array_map()
echo "<ul>", PHP_EOL;
array_map(function ($s) {
echo " <li>", h($s), "</li>", PHP_EOL;
}, $ss);
echo "</ul>", PHP_EOL, PHP_EOL;
// array_walk()
echo "<ul>", PHP_EOL;
array_walk($ss, function ($s) {
echo " <li>", h($s), "</li>", PHP_EOL;
});
echo "</ul>", PHP_EOL, PHP_EOL;
// foreach
echo "<ul>", PHP_EOL;
foreach ($ss as $s) {
echo " <li>", h($s), "</li>", PHP_EOL;
}
echo "</ul>", PHP_EOL, PHP_EOL;
どれで書いても差がありません。この例においては、function
を書く必要がないforeach
がもっとも簡潔だと言って支障ないですね。
改めて関数の説明
array_map()
array_map
— 指定した配列の要素にコールバック関数を適用するarray array_map ( callable $callback , array $array1 [, array $... ] )
array_map()
は、array1
の各要素にcallback
関数を適用した後、 その全ての要素を含む配列を返します。callback
関数が受け付けるパラメータの数は、array_map()
に渡される配列の数に一致している必要があります。
(PHP: array_map - Manualより抜萃、2018年6月24日閲覧)
ここで新たな用語「適用」(英語ではapply)が登場しました。非常にざっくりと説明すると、単に関数を呼び出して式を評価することです。
ここでは算数で出てきた式 $f(a)$ を「$a$に$f$を適用する」と読むことにします。$a$が「適用される」側です。 $g(x) = 2x$ としたとき、「$3$に$g$を適用すると$6$になる($g(3) = 6$)」です。
PHPでの実用的な例を出します。以下のような果物と値段の対応表を用意します。
$fruits = [
'apple' => 100,
'orange' => 150,
'strawberry' => 80,
];
次に、日本の消費税を計算する関数 tax
を用意します。
function tax($n): float
{
return $n * 1.08;
}
これを使って果物それぞれの消費税込みの値段を知るには、$fruits
のそれぞれにtux
を適用する式を以下のように書けばよいですね。
var_dump(tax($fruits['apple'])); // => float(108)
var_dump(tax($fruits['orange'])); // => float(162)
var_dump(tax($fruits['strawberry'])); // => float(86.4)
これはこれでよいのですが、果物の種類が増えるごとに一行書き足していかなくてはいけなくなります。せっかく一個の配列 $fruits
にまとまってるのですから、一気に計算したくなるのが人情です。
ところで、小学校や中学校の算数で、グラフを描く準備として数字の表を書いた覚えはありませんか? さきほどの $g(x) = 2x$ を $y = 2x$ と直せば、以下のような表を書いたおぼえがあります。
$x$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
$y$ | 0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 |
ここで再び array_map()
の定義を持ち出します。
array_map
— 指定した配列の要素にコールバック関数を適用する
配列の中に含まれる一個一個のことを、プログラミング言語では「要素」と呼びます。これは高校の算数の教科書で習った気がする「元」と同じく英語のelement
と対応することばで、単に分野によって定訳が異なるってだけの話です。
ことばの由来は算数の用語ですが、そんなに大事なことではないのでリンク先を読む必要は特にありません。ただのトリビアです。ただ、そのほかのパソコン用語でもelement
に対応する用語はいろいろある1ので、ややこしいですね。
さて、array_map()
のイメージは、$x$の列を「まとめて」$y$の列に変換することです。改めて上の処理を array_map()
を使って書き直してみます。
var_dump(array_map(function($n){ return tax($n);}, $fruits));
// array(3) {
// ["apple"]=>
// float(108)
// ["orange"]=>
// float(162)
// ["strawberry"]=>
// float(86.4)
// }
この式は、以下のように書いてもまったく同じことです。
var_dump(array_map('tax', $fruits));
var_dump(array_map(function($n){ return $n * 1.08; }, $fruits));
ここで気をつけてほしいのは、array_map()
は各要素に関数を適用した配列を返しますが、もとの配列変数$fruit
は何の変化がない状態のままであることです。。
さらに、 foreach
を使って書き直すと次のようになります。
$result = [];
foreach ($fruits as $name => $price) {
$result[$name] = tax($price);
}
var_dump($result);
要は、この foreach
を1行で書くためにあるのが array_map()
だと思ってください。
最後に配列を組み立てるのがarray_map()
の本質的な機能です。つまり、この記事の最初にサンプルコードとして上げたコードをarray_map()
で動かすのは、基本的に無意味です。
// array_map()
echo "<ul>", PHP_EOL;
array_map(function ($s) {
echo " <li>", h($s), "</li>", PHP_EOL;
}, $ss);
echo "</ul>", PHP_EOL, PHP_EOL;
array_map()
で呼ばれるコールバックは何も値を返してないですし、返り値を何にも使ってないですからね。
array_walk()
array_walk
— 配列の全ての要素にユーザー定義の関数を適用するbool array_walk ( array &$array , callable $callback [, mixed $userdata = NULL ] )
array 配列の各要素にユーザー定義関数 callback を適用します。
array_walk() は array の内部配列ポインタに影響されません。array_walk() はポインタの位置に関わらず配列の全てに渡って適用されます。
今度もまた「適用」の用語が出てきましたが、これはarray_map()
とはすこし趣きがことなるものです。
比較のために、もういちどarray_map()
の定義を抜萃します。
array array_map ( callable $callback , array $array1 [, array $... ] )
(PHP: array_walk - Manualより抜萃、2018年6月24日閲覧)
差がわかりましたか?
- 引数の仕様が異なる
-
array_map()
は1つ以上の任意個数の配列(array $array1 [, array $... ]
)を受け取る -
array_walk()
は配列変数のリファレンス(array &$array
)をひとつだけ受け取る
-
- コールバック関数の仕様が異なる
-
array_map()
は、array_map()
の引数として受け取ったarray $array1 [, array $... ]
のそれぞれの要素を第1引数, 第2引数, 第n引数として受け取る -
array_walk()
は、(&$item1, $key)
を受け取り、array_walk()
の第3引数を指定した場合のみコールバック関数の第3引数としても渡される
-
- 返り値の仕様が異なる
-
array_map()
はキーを維持したまま、適用結果を要素として持つ配列を返す -
array_walk()
はbool(true)
を返す2
-
以上のような差異から、array_walk()
が提供する機能はarray_map()
とは目指すものがまったくことなることがわかります。
本質的に大きくことなる特徴として、array_walk()
は配列そのものではなく、配列変数と要素のリファレンス(参照&
)を操作の対象とすることが挙げられます。
さきほどのarray_map()
と同じように、果物の消費税率をarray_walk()
で計算する例を挙げます。
<?php
$fruits = [
'apple' => 100,
'orange' => 150,
'strawberry' => 80,
];
// コールバック関数に `&` を付けるのを忘れてはいけない
array_walk($fruits, function(&$n){ $n = tax($n); }));
var_dump($fruits);
// array(3) {
// ["apple"]=>
// float(108)
// ["orange"]=>
// float(162)
// ["strawberry"]=>
// float(86.4)
// }
さきほど**array_map()
は元の配列変数を変化させないと書きましたが、今度のarray_walk()
は配列変数を変化させることができます**。
これはRubyのArray#map
とArray#map!
の関係とすこし似てますが、コールバック関数の仕様がことなるので同じように書くことはできないので注意が必要です。
array_map()
用のコールバック関数はfunction($n){ return tax($n);}
でしたが、今度はfunction(&$n){ $n = tax($n); }
です。ややこしいですね。
こんどの例もforeach
で書き直すことができます。
foreach ($fruits as $name => &$price) {
$price = tax($price);
}
unset($price); // ← この unset は省略してはいけない
var_dump($fruits);
これをわざと変化させたくないときは、こうすることもできます。
$taxed_fruits = $fruits;
foreach ($taxed_fruits as $name => &$price) {
$price = tax($price);
}
unset($price); // ← この unset は省略してはいけない
var_dump($taxed_fruits); // 変化後・税込みの値段
var_dump($fruits); // 変化前・税抜きの値段
が、わざわざリファレンス(&
)を使ってこんな書きかたをするメリットは乏しいので、ふつうに書いたほうがいいですね……。
$taxed_fruits = [];
foreach ($fruits as $name => $price) {
$taxed_fruits[$name] = tax($price);
}
var_dump($taxed_fruits); // 変化後・税込みの値段
var_dump($fruits); // 変化前・税抜きの値段
foreach
これまで紹介したarray_map()
とarray_walk()
が関数なのに対して、foreach
はPHPの言語構文です。
foreach は、配列を反復処理するための便利な方法です。 foreach が使えるのは配列とオブジェクトだけであり、 別のデータ型や初期化前の変数に対して使うとエラーになります。 この構造には二種類の構文があります。
foreach (array_expression as $value) 文 foreach (array_expression as $key => $value) 文
最初の形式は、
array_expression
で指定した配列に 関してループ処理を行います。各反復において現在の要素の値が$value
に代入され、内部配列ポインタが一つ前に進められます。(よって、次の反復では次の要素を見ることになります。)2番目の形式は、さらに各反復で現在の要素のキーを変数
$key
に代入します。オブジェクトの反復処理をカスタマイズすることもできます。
(PHP: foreach - Manualより抜萃、2018年6月24日閲覧)
PHPの入門書などでは繰り返しのための構文としてwhile
やfor
が紹介されますが、現実のPHPコードで有限回数の繰り返しをする際に利用されるのは、圧倒的にforeach
の方です。
ここまでさんざんforeach
を使ってきたのでわざわざ使用方法を説明することはないかもしれません。
配列を走査する場合は、for
文よりもforeach
文が、圧倒的にべんりです。
$ary = ['りんご', 'ごりら', 'らっぱ'];
for ($i = 0; $i < count($ary); $i++ ) {
var_dump($ary[$i]);
}
for
で書く場合は対象の配列が0
から始まる連番であること、途中に連番の抜けがある場合の処理などを考慮する必要があり面倒ですが、foreach
では基本的にその配慮は不要です。
$ary = ['りんご', 'ごりら', 'らっぱ'];
foreach ($ary as $elm) {
var_dump($elm);
}
array_map()
やarray_walk()
を使って配列を走査する簡単なパターンはforeach
で簡単に書けることは、これまで延々と説明した通りです。逆にforeach
で書けるものがarray_map()
やarray_walk()
で書けないことは多数あります。
そう、array_map()
やarray_walk()
は名前の通り、配列にしか利用できません。一方、foreach
は、それ以外のいろいろなものを走査できます。
- 配列
array
- オブジェクト
object
- Traversable
筆者の見解としては、array_map()
とかarray_walk()
とか覚えるよりも素直にforeach
で書くのがよいです。
おまけ1: foreach
, array_map()
, array_walk()
の差が際立つ例
「配列の配列の要素をソートする関数」を例としてみると、foreach
, array_map()
, array_walk()
はそれぞれ別の書きかたをする必要があります。
<?php
$as = [
[3, 4, 2, 5, 1],
['p', 'h', 'p'],
[5, 2, 1, 4, 3],
];
$x = sort_foreach($as);
$y = sort_array_map($as);
$z = sort_array_walk($as);
var_dump($x === $y && $x === $z && $y === $z);
//var_dump($x, $y, $z);
function sort_foreach(array $as)
{
foreach ($as as $k => $a) {
sort($a);
$as[$k] = $a;
}
return $as;
}
function sort_array_map(array $as)
{
return array_map(function ($a) {
sort($a);
return $a;
}, $as);
}
function sort_array_walk(array $as)
{
array_walk($as, function (&$a) {
sort($a);
});
return $as;
}
ねむいので説明はしません。
おまけ2: array_walk_recursive()
foreach
およびarray_map()
, array_walk()
は配列の要素を走査しますが、array_walk_recursive()
は配列を木構造とみなして葉まで再帰的に走査することができます。
これはarray_walk()
の親戚なので、操作対象はarray_walk()
と同じく配列変数と要素のリファレンスです。
ここで紹介するのはWebアプリケーションのエラーログにユーザーが入力したパスワードが残らないようにするための簡単な例です。
<?php
set_exception_handler(function($e) {
$backtrace = filter_backtrace($e->getTrace());
var_dump($backtrace);
exit(1);
});
$input = [
'username' => 'hogehoge',
'password' => 'fugafuga'
];
login($input);
function login(array $input)
{
$user = check_user($input['username'], $input['password']);
}
function check_user($username, $password)
{
throw new \RuntimeException("未実装です");
}
/**
* backtraceから引数に含まれるものを含めてpasswordをフィルタリングする
*/
function filter_backtrace($backtrace)
{
foreach ($backtrace as $i => $b) {
$ref = new ReflectionFunction($b['function']);
foreach ($ref->getParameters() as $j => $arg) {
if ($arg->name === 'password') {
$backtrace[$i]['args'][$j] = '(password censored)';
}
}
}
array_walk_recursive($backtrace, function (&$val, $key) {
if ($key === 'password') {
$val = '(password censored)';
}
});
return $backtrace;
}
残念ながら、この例はarray_walk_recursive()
だけで完結させることはできません。
適当に書いただけだし、飽きてきたのでコードの詳しい説明はしません。本番運用するならもうちょっと考慮しないとエラー出るよ。
おまけ3: 関連記事
配列とかリファレンスとかについてはいままでも怪気炎を吐いてきたので、興味が残ってたら読んでね。
-
PHPのリファレンス(参照&)の傾向と対策、あるいはさよなら
- 今回の記事では
array_walk()
について説明する都合上リファレンスを多用しました - リファレンス(参照渡し)は避けられるなら避けた方がいいですが、用法容量を守ってやるぶんには安全です
- 今回の記事では
-
array_mapにありがとう、さよなら
- だいたい今回と似たような話だよ
- 今回さらっと流した「コールバック(
callable
)」についての言及もあるよ - ジェネレータについても書いたよ
- array_mapと部分適用
-
部分適用ってなんだ
- 今回ちょっと触れた「適用」の観点から
array_map()
とforeach
について書いたよ
- 今回ちょっと触れた「適用」の観点から
- PHP「関数っぽいもの」列伝
あとがき
この記事が役に立ったならtadsanのpixivFANBOXを購読してね。さもなければ今後のモチベーションの維持は保障できない