Generatorを使って配列をコンソールに出す処理を書く

## 概要

再帰的に配列を作成し返す関数とGeneratorを返す関数を書き、その違いを見比べてみます。

## 経緯

任意の配列をコンソールに出す処理を書いていました。  
始めは直接ループ内で出力していましたが、非常にテストしづらいので、先に出力する文字列の配列を全部作成してから一気に出力しようと考えました。  
そこで、大きい配列を出力するためだけに同じような(あるいは大きい)配列を作成し、メモリを使うのは勿体無いので、Generatorを使って書くに至りました。  
恥ずかしながら、Generatorは今まで使った事が無かったのですが、ちゃんと書いてみると配列バージョンとGeneratorバージョンの書き方の対応がよく分かったので、メモがてら残そうと思った次第です。

## コード

### 配列を作成し返す場合のコード

```php
/**
 * 配列を表示するための文字列を取得する
 *
 * @param array $data
 * @return array
 */
function getOutputStr_Array(array $data, int $depth = 0) : array
{
    $result = [];

    $indent = str_repeat('  ', $depth);
    if (empty($data)) {
        $result[] = $indent.'[]';
        return $result;
    }
    
    $result[] = $indent.'[';

    foreach($data as $key => $val) {
        $val = normalize($val);
        
        if (is_array($val)) {
            $result[] = $indent.'  '.$key.' =>';
            $result = array_merge($result, getOutputStr_Array($val, $depth+1));
        } else if (is_object($val)) {
            if (method_exists($val, 'toArray')) {
                $result[] = $indent.'  '.$key.' =>';
                $result = array_merge($result, getOutputStr_Array($val->toArray(), $depth+1));
            } else if (method_exists($val, 'toString')) {
                $result[] = $indent.'  '.$key.' => '.$val->toString();
            } else if (method_exists($val, '__toString')) {
                $result[] = $indent.'  '.$key.' => '.$val;
            } else {
                $result[] = $indent.'  '.$key.' => '.get_class($val);
            }
        } else {
            $result[] = $indent.'  '.$key.' => '.$val;
        }
    }
    
    $result[] = $indent.']';
    return $result;
}
```

### Generatorを返す場合のコード

```php
/**
 * 配列を表示するための文字列を取得する
 *
 * @param array $data
 * @return \Generator
 */
function getOutputStr_Generator(array $data, int $depth = 0) : \Generator
{
    $indent = str_repeat('  ', $depth);
    if (empty($data)) {
        yield $indent.'[]';
        return;
    }
    
    yield $indent.'[';

    foreach($data as $key => $val) {
        $val = normalize($val);
        
        if (is_array($val)) {
            yield $indent.'  '.$key.' =>';
            foreach (getOutputStr_Generator($val, $depth+1) as $line) {
                yield $line;
            }
        } else if (is_object($val)) {
            if (method_exists($val, 'toArray')) {
                yield $indent.'  '.$key.' =>';
                foreach (getOutputStr_Generator($val->toArray(), $depth+1) as $line) {
                    yield $line;
                }
            } else if (method_exists($val, 'toString')) {
                yield $indent.'  '.$key.' => '.$val->toString();
            } else if (method_exists($val, '__toString')) {
                yield $indent.'  '.$key.' => '.$val;
            } else {
                yield $indent.'  '.$key.' => '.get_class($val);
            }
        } else {
            yield $indent.'  '.$key.' => '.$val;
        }
    }
    
    yield $indent.']';
}
```

### その他コード

`normalize`という関数は以下の内容です。

```php
function normalize(mixed $val) : mixed
{
    return match ($val) {
        '' => '\'\'',
        [] => '[]',
        null => 'null',
        false => 'false',
        true => 'true',
        default => $val,
    };
}
```

テストコードは以下の内容です。  
※ケースは最低限のみ記述しています。

```php
/**
 * @dataProvider case_getOutputStr
 *
 * @param array $data
 * @param array $expected
 * @return void
 */
public function test_getOutputStr(array $data, array $expected)
{
    $resultGen = getOutputStr_Generator($data);

    $idx = 0;
    foreach ($resultGen as $str) {
        $this->assertEquals($expected[$idx], $str);
        //echo PHP_EOL.$str;
        $idx++;
    }

    $resultArr = getOutputStr_Array($data);

    $idx = 0;
    foreach ($resultArr as $str) {
        $this->assertEquals($expected[$idx], $str);
        //echo PHP_EOL.$str;
        $idx++;
    }
}
public function case_getOutputStr()
{
    return [
        // 空配列
        [
            [],
            [
                '[]',
            ]
        ],
        // キー省略
        [
            [
                'test'
            ],
            [
                '[',
                '  0 => test',
                ']',
            ]
        ],
        // キー省略無し
        [
            [
                'key' => 'val'
            ],
            [
                '[',
                '  key => val',
                ']',
            ]
        ],
        // 入れ子
        [
            [
                [
                    'key' => 'val'
                ],
            ],
            [
                '[',
                '  0 =>',
                '  [',
                '    key => val',
                '  ]',
                ']',
            ]
        ],
        [
            [
                [
                    [
                        'key' => 'val'
                    ],
                ],
            ],
            [
                '[',
                '  0 =>',
                '  [',
                '    0 =>',
                '    [',
                '      key => val',
                '    ]',
                '  ]',
                ']',
            ]
        ],

        // 入れ子が混在
        [
            [
                [
                    'key1' => 'val1'
                ],
                [
                    [
                        'key2' => 'val2'
                    ],
                ],
            ],
            [
                '[',
                '  0 =>',
                '  [',
                '    key1 => val1',
                '  ]',
                '  1 =>',
                '  [',
                '    0 =>',
                '    [',
                '      key2 => val2',
                '    ]',
                '  ]',
                ']',
            ]
        ],

        // etc..

    ];
}
```

## 上記のコードを見比べる

### 配列に1つ追加する場合

1行分の文字列を作成する度に、配列を返す関数では`$result`という配列に追加(`$result[] = 'hoge'`)していきますが、  
Generatorを返す関数では都度`yield 'hoge'`を行っています。

### 途中でreturnする場合

配列を返す関数で、途中で`return`する箇所

```php
if (empty($data)) {
    $result[] = $indent.'[]';
    return $result;
}
```

は、Generatorを返す関数では

```php
if (empty($data)) {
    yield $indent.'[]';
    return;
}
```

のように`yield`直後に空の`return`を書くことで同じ動きを表現出来ています。  
※Generatorを返す関数では、`return`で返した値は`Generator::getReturn`で取得します。[参考](https://www.php.net/manual/ja/generator.getreturn.php)

### 再帰的に処理を呼ぶ場合

配列を返す関数では

```php
$result = array_merge($result, getOutputStr_Array($val, $depth+1));
```

と書き、`$result`に纏めて追加しています。  
Generatorを返す関数では「纏めて`yield`する」といったことはしませんので、

```php
foreach (getOutputStr_Generator($val, $depth+1) as $line) {
    yield $line;
}
```

のように再帰的に処理を呼んでいる箇所でループさせ、`yield`をします。  
※ただし、配列を返す関数で以下のように書けば[配列に1つ追加する場合](#配列に1つ追加する場合)と同じだと思えます。

```php
foreach (getOutputStr_Array($val, $depth+1) as $line) {
    $result[] = $line;
}
```

## 終わり

初めてGeneratorを使ってコードを書いてみましたが、書いた後となっては普通に配列操作をするのと大差無いと感じました。  
ただし、返り値として受け取った、単純な配列をそのままループさせるなら使えますが、受け取った後に配列関数を噛ませる等は不可能です。  
その(可能性のある)ような場合の使用を避けるか、そもそもそうならないように設計することを意識する必要がありますね。

## 追記

配列を出力するなら

```php
json_encode($array, JSON_PRETTY_PRINT);
```
を使いましょう。