13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Iterator を再帰的に処理する Generator

Last updated at Posted at 2014-06-15

array_walk_recursive の Generator 版のようなものです。

処理される Iterator は次のように Generator で実装することにします。

g.php
<?php
return call_user_func(function () {
    yield 1;
    yield 2;
    yield call_user_func(function () {
        yield 3;
        yield 4;
        yield call_user_func(function () {
            yield 5;
            yield 6;
        });
    });
});

とりあえず普通に array_walk_recursive っぽいものを作ってみます。

<?php
$recursive = function ($arr, callable $callback) use (&$recursive) {
    foreach ($arr as $key => $val) {
        if (is_array($val) || $val instanceof \Traversable) {
            $recursive($val, $callback);
        } else {
            $callback($val, $key);
        }
    }
};

$iterator = include __DIR__ . '/g.php';
$recursive($iterator, function ($val, $key) {
    echo "$key => $val,\n";
});
/*
0 => 1,
1 => 2,
0 => 3,
1 => 4,
0 => 5,
1 => 6,
*/

単純に callback のところを yield にすれば大丈夫かなーと思いましたが、そんなことはありません。

<?php
$recursive = function ($arr) use (&$recursive) {
    foreach ($arr as $key => $val) {
        if (is_array($val) || $val instanceof \Traversable) {
            $recursive($val);
        } else {
            yield $key => $val;
        }
    }
};

$iterator = include __DIR__ . '/g.php';
foreach ($recursive($iterator) as $key => $val) {
    echo "$key => $val\n";
}
/*
0 => 1
1 => 2
*/

$recursive($val) は Generator を返しているので、呼び出し側でそれを回してやる必要があります。

<?php
$recursive = function ($arr) use (&$recursive) {
    foreach ($arr as $key => $val) {
        if (is_array($val) || $val instanceof \Traversable) {
            foreach ($recursive($val) as $k => $v) {
                yield $k => $v;
            }
        } else {
            yield $key => $val;
        }
    }
};

$iterator = include __DIR__ . '/g.php';
foreach ($recursive($iterator) as $key => $val) {
    echo "$key => $val\n";
}
/*
0 => 1
1 => 2
0 => 3
1 => 4
0 => 5
1 => 6
*/

なお、いくつかの Iterator の実装では Iterator の current が Iterator 自身になることがあります、そのような Iterator にこの方法は使えません。無限に再帰してしまいます。

<?php
$iterator = new \DirectoryIterator(__DIR__);
foreach ($iterator as $it) {
    var_dump($iterator === $it);
}
/*
bool(true)
bool(true)
bool(true)
*/

再帰の条件を付け足してそのような Iterator が渡されても無限再帰しないようにします。

<?php
$recursive = function ($arr) use (&$recursive) {
    foreach ($arr as $key => $val) {
        if (is_array($val) || ($val instanceof \Traversable && $arr !== $val)) {
            foreach ($recursive($val) as $k => $v) {
                yield $k => $v;
            }
        } else {
            yield $key => $val;
        }
    }
};

$iterator = new \DirectoryIterator(__DIR__);
foreach ($recursive($iterator) as $key => $val) {
    echo "$key => $val\n";
}
/*
0 => .
1 => ..
2 => 01.php
3 => 02.php
 :
*/

ところで、SPL に再帰的な Iterator をフラットに反復する Iterator があります。RecursiveIteratorIterator です。

RecursiveIteratorIterator は RecursiveIterator をインプリメントした再帰的な Iterator をフラットに反復します。
ただの配列なら RecursiveArrayIterator と組み合わせて次のようにできます。

<?php
$arr = [1, 2, [3, 4, [5, 6]]];

$iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($arr));

foreach ($iterator as $key => $val) {
    echo "$key => $val,\n";
}
/*
0 => 1,
1 => 2,
0 => 3,
1 => 4,
0 => 5,
1 => 6,
*/

Iterator を返す Iterator は RecursiveIterator ではないため、これをそのまま使うことはできません。
折角なので RecursiveArrayIterator の Iterator 版を作ってみます(もはや Generator はあまり関係ないですが)。

RecursiveArrayIterator に対して RecursiveIteratorIterator かなーと思ったのですがその名前は使えないので RecursiveRecursiveIterator としました。

IteratorIterator を継承しています。Traversable を Iterator にするためのものですが、既存の Iterator に Decorator 的なことをやるために使えます。

<?php
class RecursiveRecursiveIterator extends \IteratorIterator implements \RecursiveIterator
{
    public function hasChildren()
    {
        return $this->current() instanceof \Traversable && $this !== $this->current();
    }

    public function getChildren()
    {
        if ($this->current() instanceof \RecursiveIterator) {
            return $this->current();
        } else {
            return new static($this->current());
        }
    }
}

$iterator = include __DIR__ . '/g.php';
$recursiveIterator = new \RecursiveIteratorIterator(
    new \RecursiveRecursiveIterator($iterator)
);

foreach ($recursiveIterator as $key => $val) {
    echo "$key => $val,\n";
}
/*
0 => 1,
1 => 2,
0 => 3,
1 => 4,
0 => 5,
1 => 6,
*/
13
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?