Рекурсивные генераторы в PHP
Введение
Так как версия 5.5 на PHP такая замечательная вещь, как 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
sample. Псевдокод будет:
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
значение.
Бонус: получение последовательности фибоначчи - всего лишь образец для более общего вопроса: как использовать генераторы с рекурсией в общем случае? Конечно, я могу использовать стандартный Iterator для этого или переписать мою функцию, чтобы избежать рекурсии. Но я хочу добиться этого с помощью генераторов. Это возможно? Стоит ли это использовать таким образом?
Ответы
Ответ 1
Наконец-то я определил реальное использование рекурсивных генераторов.
Недавно я изучал QuadTree datastructures. Для тех, кто не знаком с QuadTrees, они используют древовидную структуру дерева для геопространственной индексации и позволяют быстро искать поиск всех точек/местоположений в пределах определенной ограничивающей рамки.
Каждый node в QuadTree представляет сегмент отображаемой области и действует как ведро, в котором хранятся местоположения... но ведро с ограниченным размером. Когда ведро переполняется, QuadTree node разделяет 4 дочерних узла, представляющих северо-западную, северо-восточную, юго-западную и юго-восточную области родительского node, и начинает заполнять их.
При поиске местоположений, попадающих в указанный ограничивающий прямоугольник, процедура поиска начинается с верхнего уровня node, проверяя все местоположения в этом ковше; затем рекурсирует в дочерние узлы, проверяя, пересекаются ли они с ограничивающей рамкой или охвачены ограничивающей рамкой, проверяя каждый QuadTree node внутри этого набора, а затем снова рекурсивный вниз через дерево. Каждый node может возвращать ни одного, ни одного или нескольких местоположений.
Я реализовал базовый QuadTree в PHP, предназначенный для возврата массива результатов; затем понял, что это может быть допустимый прецедент для рекурсивного генератора, поэтому я реализовал GeneratorQuadTree, к которому можно получить доступ в цикле foreach(), дающем единственный результат каждая итерация.
Кажется, это более эффективный вариант использования для рекурсивных генераторов, потому что он является действительно рекурсивной функцией поиска и потому, что каждый генератор может возвращать ни один, ни один или несколько результатов, а не один результат. Эффективно каждый вложенный генератор обрабатывает часть поиска, подавая свои результаты обратно через дерево через своего родителя.
Этот код слишком много для публикации здесь; но вы можете взглянуть на реализацию на github.
Он меньше медленнее, чем версия, отличная от генератора (но не значительно): основным преимуществом является сокращение памяти, поскольку оно не просто возвращает массив переменной величины (что может быть значительным преимуществом в зависимости от количества результатов возвращается). Самым большим недостатком является то, что результаты не могут быть легко отсортированы (моя версия, отличная от генератора, выполняет функцию usort() в массиве результатов после ее возврата).
Ответ 2
Итак, проблема, с которой я столкнулась при попытке создания рекурсивной функции генератора, заключается в том, что, пройдя свой первый уровень глубины, каждый последующий выход уступает его родительскому вызову, а не реализации итерации (цикл).
Как и в 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
Ответ 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;
}
Ответ 4
Благодаря Mark Baker Я понял, что реальный прецедент для этого трудно найти (если не невозможно).
Генератор не является функцией, да (может быть, это меня смутило) - поэтому "вызов" внутри внутри с "рекурсивным" параметром почти бесполезен. Мы можем действовать, как предположил Марк, но, немного подумав, я закончил этот код для моей последовательности Фибоначчи:
function xfibonacci($n)
{
$recursion = function($n) use (&$recursion)
{
return $n<2?$n:$recursion($n-1)+$recursion($n-2);
};
for($i=0; $i<$n; $i++)
{
yield $recursion($i);
}
}
//...
foreach(xfibonacci(6) as $i)
{
echo('num is: '.$i.PHP_EOL);//0,1,1,2,3,5
}
- и, что более важно, его можно легко преобразовать в обычный случай, когда мы хотим иметь генератор, который опирается на некоторую рекурсивную функцию генерации последовательности. Итак, это будет:
function xrecursive($n, callable $callback, $args=null)
{
for($i=0; $i<$n; $i++)
{
yield is_array($args)?
call_user_func_array($callback, array_merge([$i], $args)):
call_user_func_array($callback, [$i]);
}
}
- с образцом использования:
//factorial:
foreach(xrecursive(6, $f=function($n) use (&$f){return $n?$n*$f($n-1):1;}) as $i)
{
echo('num is: '.$i.PHP_EOL);//1,1,2,6,24,120
}
-so, генератор, который полагается на функцию рекурсивной генерации последовательности, может быть разделен на реализацию. Это не полный ответ для оригинального вопроса (так как могут быть способы реализации желаемого поведения) - но в то же время я думаю, что это более правильный способ использования генератора в контексте генерации рекурсивных последовательностей.
Ответ 5
Если вы сначала хотите создать генератор, вы можете использовать итеративную версию фибоначчи:
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 "
}
Ответ 6
Здесь рекурсивный генератор для комбинаций (порядок несущественный, без замены):
<?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 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
То же самое, что и без урона.
Ответ 7
Недавно возникла проблема, которая требовала "рекурсивных" генераторов или генератора. В итоге я написал небольшую функцию, которая преобразует делегированные вызовы генераторов в один генератор.
Я превратил его в пакет, чтобы вы могли просто потребовать его с композитором или проверить источник здесь: 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
}
Ознакомьтесь с этой ссылкой выше. В нем есть примеры.
Ответ 8
Короткий ответ: рекурсивные генераторы просты. Пример прохода по дереву:
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 . " ";
}
Ответ 9
Я предлагаю два решения для числа Фибоначчи с рекурсией и без нее:
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();