32

介绍

自 PHP 5.5 版本以来,就有了像generators这样伟大的东西。我不会重复官方手册页,但它们对于迭代器的简短定义非常有用。最知名的样本是:

function xrange($from, $till, $step)
{
   if ($from>$till || $step<=0)
   {
      throw new InvalidArgumentException('Invalid range initializers');
   }

   for ($i = $from; $i < $till; $i += $step)
   {
      yield $i;
   }
}

//...

foreach (xrange(2, 13, 3) as $i)
{
   echo($i.PHP_EOL); // 2,5,8,11
}

而生成器实际上不是一个函数,而是一个具体类的实例:

get_class(xrange(1, 10, 1)); // Generator


问题

完成了 RTM 的东西,现在继续我的问题。想象一下,我们要创建斐波那契数列的生成器。通常,要获得这些,我们可以使用简单的函数:

function fibonacci($n)
{
   if(!is_int($n) || $n<0)
   {
      throw new InvalidArgumentException('Invalid sequence limit');
   }
   return $n < 2 ? $n : fibonacci($n-1) + fibonacci($n-2);
}

var_dump(fibonacci(6)); // 8

让我们将其转换为具有序列且不仅是最后一个成员的东西:

function fibonacci($n)
{
   if (!is_int($n) || $n<0)
   {
      throw new InvalidArgumentException('Invalid sequence limit');
   }
   if ($n<2)
   {
      return range(0, $n);
   }
   $n1 = fibonacci($n-1);
   $n2 = fibonacci($n-2);
   return array_merge($n1, [array_pop($n1)+array_pop($n2)]);
}

//...

foreach (fibonacci(6) as $i)
{
   echo($i.PHP_EOL); // 0,1,1,2,3,5,8
}

我们现在有一个返回完整序列数组的函数


问题

最后,问题部分:如何转换我的最新fibonacci函数,以便它产生我的值,而不是将它们保存在数组中?我的$n可能很大,所以我想利用生成器的好处,比如在xrange示例中。伪代码将是:

function fibonacci($n)
{
   if (!is_int($n) || $n<0)
   {
      throw new InvalidArgumentException('Invalid sequence limit');
   }

   if ($n<2)
   {
      yield $n;
   }

   yield fibonacci($n-2) + fibonacci($n-1);
}

但这显然是废话,因为我们不能这样处理它,因为递归会导致类对象Generator而不是int值。

奖励:获得斐波那契序列只是更一般问题的一个示例:在常见情况下如何使用具有递归的生成器?当然,我可以为此使用标准迭代器或重写我的函数以避免递归。但我想用生成器来实现。这可能吗?这值得努力使用这种方式吗?

4

8 回答 8

20

因此,我在尝试创建递归生成器函数时遇到的问题是,一旦超过了第一个深度级别,每个后续的 yield 都会屈服于其父调用而不是迭代实现(循环)。

从 php 7 开始,添加了一个新功能,允许您后续的生成器函数中生成。这是新的生成器委托功能:https ://wiki.php.net/rfc/generator-delegation

这允许我们从后续的递归调用中产生,这意味着我们现在可以使用生成器有效地编写递归函数。

$items = ['what', 'this', 'is', ['is', 'a', ['nested', 'array', ['with', 'a', 'bunch',  ['of', ['values']]]]]];

function processItems($items)
{
    foreach ($items as $value)
    {
        if (is_array($value))
        {
            yield from processItems($value);
            continue;
        }
        yield $value;
    }
}

foreach (processItems($items) as $item)
{
    echo $item . "\n";
}

这给出了以下输出..

what
this
is
is
a
nested
array
with
a
bunch
of
values
于 2015-06-29T10:37:36.963 回答
9

I've finally identified a real-world use for recursive generators.

I've been exploring QuadTree datastructures recently. For those not familiar with QuadTrees, they're a tree-based datastructure use for geospatial indexing, and allowing a fast search lookup of all points/locations within a defined bounding box. Each node in the QuadTree represents a segment of the mapped region, and acts as a bucket in which locations are stored... but a bucket of restricted size. When a bucket overflows, the QuadTree node splits off 4 child nodes, representing the North-west, North-east, South-west and South-east areas of the parent node, and starts to fill those.

When searching for locations falling within a specified bounding box, the search routine starts at the top-level node, testing all the locations in that bucket; then recurses into the child nodes, testing whether they intersect with the bounding box, or are encompassed by the bounding box, testing each QuadTree node within that set, then recursing again down through the tree. Each node may return none, one or many locations.

I implemented a basic QuadTree in PHP, designed to return an array of results; then realised that it might be a valid use case for a recursive generator, so I implemented a GeneratorQuadTree that can be accessed in a foreach() loop yielding a single result each iteration.

It seems a much more valid use-case for recursive generators because it is a truly recursive search function, and because each generator may return none, one or many results rather than a single result. Effectively, each nested generator is handling a part of the search, feeding its results back up through the tree through its parent.

The code is rather too much to post here; but you can take a look at the implementation on github.

It's fractionally slower than the non-generator version (but not significantly): the main benefit is reduction in memory because it isn't simply returning an array of variable size (which can be a significant benefit depending on the number of results returned). The biggest drawback is the fact that the results can't easily be sorted (my non-generator version does a usort() on the results array after it's returned).

于 2013-12-06T19:04:38.357 回答
3
function fibonacci($n)
{
    if($n < 2) {
        yield $n;
    }

    $x = fibonacci($n-1);
    $y = fibonacci($n-2);
    yield $x->current() + $y->current();
}


for($i = 0; $i <= 10; $i++) {
    $x = fibonacci($i);
    $value = $x->current();
    echo $i , ' -> ' , $value, PHP_EOL;
}
于 2013-11-07T12:29:50.247 回答
1

如果您首先想制作一个生成器,您不妨使用斐波那契的迭代版本:

function fibonacci ($from, $to)
{
  $a = 0;
  $b = 1;
  $tmp;
  while( $to > 0 ) {
    if( $from > 0 )
      $from--;
    else
      yield $a;

    $tmp = $a + $b;
    $a=$b;
    $b=$tmp;
    $to--;
  }
}

foreach( fibonacci(10,20) as $fib ) {  
    print "$fib "; // prints "55 89 144 233 377 610 987 1597 2584 4181 " 
}
于 2013-11-07T14:52:58.970 回答
0

这是组合的递归生成器(顺序不重要,无需替换):

<?php

function comb($set = [], $size = 0) {
    if ($size == 0) {
        // end of recursion
        yield [];
    }
    // since nothing to yield for an empty set...
    elseif ($set) {
        $prefix = [array_shift($set)];

        foreach (comb($set, $size-1) as $suffix) {
            yield array_merge($prefix, $suffix);
        }

        // same as `yield from comb($set, $size);`
        foreach (comb($set, $size) as $next) {
            yield $next;
        }
    }
}

// let's verify correctness
assert(iterator_to_array(comb([0, 1, 2, 3, 4], 3)) == [
    [0, 1, 2], [0, 1, 3], [0, 1, 4], [0, 2, 3], [0, 2, 4],
    [0, 3, 4], [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]
]);

foreach (comb([0, 1, 2, 3], 3) as $combination) {
    echo implode(", ", $combination), "\n";
}

输出:

0, 1, 2
0, 1, 3
0, 2, 3
1, 2, 3

同样的事情不屈服。

于 2016-02-09T09:51:52.017 回答
0

简短的回答:递归生成器很简单。穿过树的例子:

class Node {

    public function getChildren() {
        return [ /* array of children */ ];
    }

    public function walk() {
        yield $this;

        foreach ($this->getChildren() as $child) {
            foreach ($child->walk() as $return) {
                yield $return;
            };
        }
    }
}

就是这样。

关于斐波那契的长答案:

生成器是与foreach (generator() as $item) { ... }. 但是 OP 希望fib()函数返回int,但同时他希望它返回generator以用于foreach. 这非常令人困惑。

可以为斐波那契实现递归生成器解决方案。我们只需要在fib()函数内部放置一个循环,该循环确实yield将序列的每个成员。由于生成器应该与foreach一起使用,它看起来真的很奇怪,我认为它不是有效的,但它是:

function fibGenerator($n) {
    if ($n < 2) {
        yield $n;
        return;
    }

    // calculating current number
    $x1 = fibGenerator($n - 1);
    $x2 = fibGenerator($n - 2);
    $result = $x1->current() + $x2->current();

    // yielding the sequence
    yield $result;
    yield $x1->current();
    yield $x2->current();

    for ($n = $n - 3; $n >= 0; $n--) {
        $res = fibGenerator($n);
        yield $res->current();
    }
}

foreach (fibGenerator(15) as $x) {
    echo $x . " ";
}
于 2017-08-09T12:59:44.500 回答
0

最近遇到了一个需要“递归”生成器或生成器委托的问题。我最终编写了一个小函数,将委托的生成器调用转换为单个生成器。

我把它变成了一个包,所以你可以用 composer 来要求它,或者在这里查看源代码:hedronium/generator-nest

代码:

function nested(Iterator $generator)
{
    $cur = 0;
    $gens = [$generator];

    while ($cur > -1) {
        if ($gens[$cur]->valid()) {
            $key = $gens[$cur]->key();
            $val = $gens[$cur]->current();
            $gens[$cur]->next();
            if ($val instanceof Generator) {
                $gens[] = $val;
                $cur++;
            } else {
                yield $key => $val;
            }
        } else {
            array_pop($gens);
            $cur--;
        }
    }
}

你像这样使用它:

foreach (nested(recursive_generator()) as $combination) {
    // your code
}

查看上面的链接。它有例子。

于 2016-12-22T11:38:24.903 回答
-1

我为斐波那契数提供了两种解决方案,有和没有递归:

function fib($n)
{
    return ($n < 3) ? ($n == 0) ? 0 : 1 : fib($n - 1) + fib($n - 2);
}
function fib2()
{
    $a = 0;
    $b = 1;
    for ($i = 1; $i <= 10; $i++)
    {
        echo $a  . "\n";
        $a = $a + $b;
        $b = $a - $b;
    }
}

for ($i = 0; $i <= 10; $i++)
{
    echo fib($i) . "\n";
}

echo fib2();
于 2013-11-15T08:34:23.223 回答