foreach, array_map, array_walkの比較

PHPの`array_map()`と`array_walk()`は引数順序の一貫性のなさが槍玉に上げられがちなので、せっかくなので`foreach`を含めて、まとめて解説します。

## はじめに三行で

 * まあ`foreach`で書くのが簡単じゃね
 * `array_walk_recursive()`で`foreach`には手の届かないことができるかもね
 * [tadsanのpixivFANBOX](https://www.pixiv.net/fanbox/creator/105589)もよろしく

### 早見表

| 関数／文                 | 操作対象                                 | 特徴                                                                     |
|--------------------------+------------------------------------------+--------------------------------------------------------------------------|
| `array_map()`            | **複数**の配列                           | 要素にコールバック関数を適用した結果をまとめた配列を返す                 |
| `array_walk()`           | 一つの**配列変数のリファレンス**の要素   | コールバック関数から要素ごとのリファレンスを直接操作することができる                         |
| `array_walk_recursive()` | 一つの**配列変数のリファレンス**の子要素 | 配列に含まれる配列の子要素も含めてリファレンスを直接操作することができる |
| `foreach`                | 一つの**イテレータ**または配列(または配列の要素のリファレンス)          | 配列以外のイテレータも走査できる。文なので結果は返さないが、配列のキーまたはリファレンスを使って要素を変更することができる |

今回は細かく説明しませんが、「配列」と「配列変数のリファレンス」には微妙な差があります。

## `array_map()`と`array_walk()`の差がわかりにくいケース

この例を見ると、`array_map()`, `array_walk()`, `foreach`のどれを使っても大差ないように見えます。

```php
<?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` — 指定した配列の要素にコールバック関数を適用する
>
> ```php
> array array_map ( callable $callback , array $array1 [, array $... ] )
> ```
>
> `array_map()` は、`array1` の各要素に `callback` 関数を適用した後、 その全ての要素を含む配列を返します。 `callback` 関数が受け付けるパラメータの数は、 `array_map()` に渡される配列の数に一致している必要があります。

*([PHP: array_map - Manual](http://php.net/manual/ja/function.array-map.php)より抜萃、2018年6月24日閲覧)*

ここで新たな用語「**適用**」(英語では[*apply*](https://en.wikipedia.org/wiki/Apply))が登場しました。非常にざっくりと説明すると、単に関数を呼び出して式を評価することです。

ここでは算数で出てきた式 $f(a)$ を「$a$に$f$を適用する」と読むことにします。$a$が「適用される」側です。 $g(x) = 2x$ としたとき、「$3$に$g$を適用すると$6$になる($g(3) = 6$)」です。

PHPでの実用的な例を出します。以下のような果物と値段の対応表を用意します。

```php
$fruits = [
    'apple' => 100,
    'orange' => 150,
    'strawberry' => 80,
];
```

次に、日本の消費税を計算する関数 `tax` を用意します。

```php
function tax($n): float
{
    return $n * 1.08;
}
```

これを使って果物それぞれの消費税込みの値段を知るには、`$fruits`のそれぞれに`tux`を適用する式を以下のように書けばよいですね。

```php
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` — 指定した配列の要素にコールバック関数を適用する

配列の中に含まれる一個一個のことを、プログラミング言語では「要素」と呼びます。これは[高校の算数の教科書](https://ja.wikibooks.org/wiki/%E9%AB%98%E7%AD%89%E5%AD%A6%E6%A0%A1%E6%95%B0%E5%AD%A6A/%E9%9B%86%E5%90%88%E3%81%A8%E8%AB%96%E7%90%86)で習った気がする「[元](https://ja.wikipedia.org/wiki/%E5%85%83_(%E6%95%B0%E5%AD%A6))」と同じく英語の<q>element</q>と対応することばで、単に分野によって定訳が異なるってだけの話です。

ことばの由来は算数の用語ですが、そんなに大事なことではないのでリンク先を読む必要は特にありません。ただのトリビアです。ただ、そのほかのパソコン用語でも<q>element</q>に対応する用語はいろいろある[^1]ので、ややこしいですね。

さて、`array_map()`のイメージは、$x$の列を「まとめて」$y$の列に変換することです。改めて上の処理を `array_map()` を使って書き直してみます。

```php
var_dump(array_map(function($n){ return tax($n);}, $fruits));
// array(3) {
//   ["apple"]=>
//   float(108)
//   ["orange"]=>
//   float(162)
//   ["strawberry"]=>
//   float(86.4)
// }
```

この式は、以下のように書いてもまったく同じことです。

```php
var_dump(array_map('tax', $fruits));
var_dump(array_map(function($n){ return $n * 1.08; }, $fruits));
```

ここで気をつけてほしいのは、`array_map()`は各要素に関数を適用した配列を返しますが、**もとの配列変数`$fruit`は何の変化がない状態のまま**であることです。。

--------

さらに、 `foreach` を使って書き直すと次のようになります。

```php
$result = [];
foreach ($fruits as $name => $price) {
    $result[$name] = tax($price);
}

var_dump($result);
```

要は、この `foreach` を1行で書くためにあるのが `array_map()` だと思ってください。

最後に配列を組み立てるのが`array_map()`の本質的な機能です。つまり、この記事の最初にサンプルコードとして上げたコードを`array_map()`で動かすのは、基本的に無意味です。

```php
// 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` — 配列の全ての要素にユーザー定義の関数を適用する
>
> ```php
> bool array_walk ( array &$array , callable $callback [, mixed $userdata = NULL ] )
> ```
> array 配列の各要素にユーザー定義関数 callback を適用します。
>
> array_walk() は array の内部配列ポインタに影響されません。array_walk() はポインタの位置に関わらず配列の全てに渡って適用されます。

今度もまた「適用」の用語が出てきましたが、これは`array_map()`とはすこし趣きがことなるものです。

比較のために、もういちど`array_map()`の定義を抜萃します。

> ```php
> array array_map ( callable $callback , array $array1 [, array $... ] )
> ```

*([PHP: array_walk - Manual](http://php.net/manual/ja/function.array-walk.php)より抜萃、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
<?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`で書き直すことができます。

```php
foreach ($fruits as $name => &$price) {
    $price = tax($price);
}
unset($price); // ← この unset は省略してはいけない

var_dump($fruits);
```

これをわざと変化させたくないときは、こうすることもできます。

```php
$taxed_fruits = $fruits;

foreach ($taxed_fruits as $name => &$price) {
    $price = tax($price);
}
unset($price); // ← この unset は省略してはいけない

var_dump($taxed_fruits); // 変化後・税込みの値段
var_dump($fruits); // 変化前・税抜きの値段
```

が、わざわざリファレンス(`&`)を使ってこんな書きかたをするメリットは乏しいので、ふつうに書いたほうがいいですね……。

```php
$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 が使えるのは配列とオブジェクトだけであり、 別のデータ型や初期化前の変数に対して使うとエラーになります。 この構造には二種類の構文があります。
>
> ```php
> foreach (array_expression as $value)
>     文
>
> foreach (array_expression as $key => $value)
>     文
> ```
>
> 最初の形式は、`array_expression`で指定した配列に 関してループ処理を行います。各反復において現在の要素の値が `$value`に代入され、内部配列ポインタが一つ前に進められます。(よって、次の反復では次の要素を見ることになります。)
>
> 2番目の形式は、さらに各反復で現在の要素のキーを変数`$key`に代入します。
>
> [オブジェクトの反復処理をカスタマイズ](http://php.net/manual/ja/language.oop5.iterations.php)することもできます。

*([PHP: foreach - Manual](http://php.net/manual/ja/control-structures.foreach.php)より抜萃、2018年6月24日閲覧)*

PHPの入門書などでは繰り返しのための構文として`while`や`for`が紹介されますが、現実のPHPコードで**有限回数の**繰り返しをする際に利用されるのは、圧倒的に`foreach`の方です。

ここまでさんざん`foreach`を使ってきたのでわざわざ使用方法を説明することはないかもしれません。

配列を走査する場合は、`for`文よりも`foreach`文が、圧倒的にべんりです。

```php
$ary = ['りんご', 'ごりら', 'らっぱ'];

for ($i = 0; $i < count($ary); $i++ ) {
    var_dump($ary[$i]);
}
```

`for`で書く場合は対象の配列が`0`から始まる連番であること、途中に連番の抜けがある場合の処理などを考慮する必要があり面倒ですが、`foreach`では基本的にその配慮は不要です。

```php
$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`
    * [PHP: オブジェクトの反復処理 - Manual](http://php.net/manual/ja/language.oop5.iterations.php)
 * [Traversable](http://php.net/manual/ja/class.traversable.php)
    * [Generator](http://php.net/manual/ja/class.generator.php)
    * [Iterator](http://php.net/manual/ja/class.iterator.php)
        * [SPLイテレータ](http://php.net/manual/ja/spl.iterators.php)
    * [IteratorAggregate](http://php.net/manual/ja/class.iteratoraggregate.php)

筆者の見解としては、**`array_map()`とか`array_walk()`とか覚えるよりも素直に`foreach`で書くのがよい**です。

---------

## おまけ1: `foreach`, `array_map()`, `array_walk()`の差が際立つ例

「配列の配列の要素をソートする関数」を例としてみると、`foreach`, `array_map()`, `array_walk()`はそれぞれ別の書きかたをする必要があります。

```php
<?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()`](http://php.net/manual/ja/function.array-walk-recursive.php)は配列を木構造とみなして葉まで再帰的に走査することができます。

これは`array_walk()`の親戚なので、操作対象は`array_walk()`と同じく配列変数と要素のリファレンスです。

ここで紹介するのはWebアプリケーションのエラーログにユーザーが入力したパスワードが残らないようにするための簡単な例です。

```php
<?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のリファレンス(参照&)の傾向と対策、あるいはさよなら](https://qiita.com/tadsan/items/74f992dcc48216b571bd)
    * 今回の記事では`array_walk()`について説明する都合上リファレンスを多用しました
    * リファレンス(参照渡し)は避けられるなら避けた方がいいですが、用法容量を守ってやるぶんには安全です
 * [array_mapにありがとう、さよなら](https://qiita.com/tadsan/items/bfd1acb7c35a9a6fe452)
    * だいたい今回と似たような話だよ
    * 今回さらっと流した「コールバック(`callable`)」についての言及もあるよ
    * ジェネレータについても書いたよ
 * [array_mapと部分適用](https://qiita.com/tadsan/items/475e8dc4657d771eb304)
 * [部分適用ってなんだ](https://qiita.com/tadsan/items/cec2851f70838e42be46)
    * 今回ちょっと触れた「適用」の観点から`array_map()`と`foreach`について書いたよ
 * [PHP「関数っぽいもの」列伝](https://qiita.com/tadsan/items/0d1e79b4baff509e7df1)

------

## あとがき

この記事が役に立ったなら[tadsanのpixivFANBOX](https://www.pixiv.net/fanbox/creator/105589)を購読してね。~~さもなければ今後のモチベーションの維持は保障できない~~

## 注釈

[^1]: HTMLで一般にタグと呼ばれるものも<q>element</q>(日本語での定訳は「要素」)と呼ばれます。タグとは`<a>`と`</a>`のような目印のことで、 `<a href="https://example.com/">ほげほげ</a>` のような塊が「要素」(この場合は`a要素`)です。
[^2]: マニュアルには実行に失敗した場合は `false` を返すと説明されてるのですが、どんな場合が失敗なのかよくわからないです。