Générateurs récursifs en PHP

Introduction

Depuis la version 5.5 en PHP, il existe des générateurs . Je ne vais pas répéter la page de manuel officielle, mais ils sont une grande chose pour la définition courte des itérateurs. L'échantillon le plus connu est:

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
}

Et generator n'est en fait pas une fonction, mais une instance d'une classe concrète:

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


Le problème

Fait avec des trucs RTM, passons maintenant à ma question. Imaginons que nous voulons créer générateur de nombres de Fibonacci. Normalement, pour les obtenir, nous pouvons utiliser une fonction simple:

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

Transformons ceci en quelque chose, qui contient la séquence et pas seulement son dernier membre:

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
}

Nous avons maintenant une fonction qui renvoie un tableau avec une séquence complète


La question

Enfin, la partie question: Comment puis-je transformer ma dernière fonction fibonacci afin qu'elle donne mes valeurs, ne les contenant pas dans un tableau? Mon $n peut être gros, si je veux utiliser les avantages des générateurs, comme dans xrange exemple. Le Pseudo-code sera:

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);
}

Mais ceci, évidemment, est de la merde puisque nous ne pouvons pas le gérer de cette façon parce que la récursivité provoquera l'objet de la classe Generator et non la valeur int.

Bonus: obtenir la séquence de fibonacci est juste un exemple pour une question plus générale: comment utiliser des générateurs avec récursivité dans le cas commun? Bien sûr, je peux utiliser standard Iterator pour cela ou réécrire ma fonction pour éviter la récursivité. Mais je veux y parvenir avec des générateurs. Est-ce possible? Est-ce que cela vaut la peine d'utiliser cette façon?

28
demandé sur Funk Forty Niner 2013-11-07 16:06:17

9 réponses

J'ai finalement identifié une utilisation réelle pour les générateurs récursifs.

J'ai récemment exploré QuadTree datastructures. Pour ceux qui ne sont pas familiers avec QuadTrees, ils sont une utilisation de datastructure basée sur l'arborescence pour l'indexation géospatiale, et permettant une recherche rapide de tous les points/emplacements dans une zone de délimitation définie. Chaque nœud du QuadTree représente un segment de la région mappée et agit comme un compartiment dans lequel les emplacements sont stockés... mais un seau de taille restreinte. Lorsqu'un compartiment déborde, le nœud QuadTree se sépare de 4 nœuds enfants, représentant les zones Nord-Ouest, Nord-Est, Sud-Ouest et Sud-Est du nœud parent, et commence à les remplir.

Lors de la recherche d'emplacements relevant d'une zone de délimitation spécifiée, la routine de recherche commence au nœud de niveau supérieur, testant tous les emplacements de ce compartiment; puis revient dans les nœuds enfants, testant s'ils se croisent avec la zone de délimitation ou sont englobés par la zone de délimitation, testant chaque nœud QuadTree dans cet ensemble, puis réapparaît à travers l'arbre. Chaque nœud peut renvoyer aucun, un ou plusieurs emplacements.

J'ai implémenté un basique QuadTree en PHP, conçu pour renvoyer un tableau de résultats; puis j'ai réalisé que ce pourrait être un cas d'utilisation valide pour un générateur récursif, donc j'ai implémenté un GeneratorQuadTree accessible dans une boucle foreach() donnant un seul résultat à chaque itération.

Il semble un cas d'utilisation beaucoup plus valide pour récursif générateurs parce que c'est une fonction de recherche vraiment récursive, et parce que chaque générateur peut renvoyer aucun, un ou plusieurs résultats plutôt qu'un seul résultat. En effet, chaque générateur imbriqué gère une partie de la recherche, alimentant ses résultats à travers l'arbre via son parent.

Le code est un peu trop à poster ici; mais vous pouvez jeter un oeil à l'implémentation sur github.

Il est fractionnellement plus lent que la version non-générateur (mais pas significativement): le principal avantage est la réduction de la mémoire car il ne renvoie pas simplement un tableau de taille variable (ce qui peut être un avantage significatif en fonction du nombre de résultats retournés). Le plus gros inconvénient est le fait que les résultats ne peuvent pas facilement être triés (ma version non-générateur fait un usort() sur le tableau de résultats après son retour).

10
répondu Mark Baker 2013-12-06 19:04:38

Donc, le problème que j'ai rencontré en essayant de créer une fonction de générateur récursif, c'est qu'une fois que vous dépassez votre premier niveau de profondeur, chaque rendement suivant cède à son appel parent plutôt qu'à l'implémentation d'itération (la boucle).

Depuis php 7, une nouvelle fonctionnalité a été ajoutée qui vous permet de générer à partir de une fonction de générateur ultérieure. C'est la nouvelle Générateur de Délégation fonction: https://wiki.php.net/rfc/generator-delegation

Ce nous permet de céder à partir d'appels récursifs ultérieurs, ce qui signifie que nous pouvons maintenant écrire efficacement des fonctions récursives avec l'utilisation de générateurs.

$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";
}

Cela donne la sortie suivante..

what
this
is
is
a
nested
array
with
a
bunch
of
values
10
répondu Lee Davis 2015-06-29 10:37:36
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;
}
2
répondu Mark Baker 2013-11-07 12:29:50

Merci à Mark Baker j'ai réalisé que le cas d'utilisation réel pour cela est difficile à trouver (sinon impossible).

Générateur n'est pas une fonction, oui (peut-être que la confusion moi) - si "appel" à l'intérieur avec "récursive" paramètre est près inutile. Nous pouvons agir comme Mark l'a suggéré, mais après avoir réfléchi un peu, j'ai fini avec ce code pour mon exemple de séquence Fibonacci:

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
}

- et, ce qui est plus important, qu'il peut être facilement converti en cas commun, lorsque nous vous voulez avoir un générateur, qui repose sur une fonction de génération de séquence récursive. Donc ce sera:

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]);
   }  
}

- avec échantillon d'utilisation:

//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
}

- ainsi, Tout générateur , qui repose sur une fonction de génération de séquence récursive, pourrait être séparé de son implémentation. Ce n'est pas une réponse complète à la question originale (car il existe peut-être des moyens d'implémenter le comportement souhaité ) - mais en même temps, je pense que c'est une façon plus correcte d'utiliser generator dans le contexte de récursif la génération de séquences.

2
répondu Alma Do 2017-05-23 11:47:10

Si vous voulez faire un générateur, vous pouvez aussi bien utiliser la version itérative de fibonacci:

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 " 
}
2
répondu Sylwester 2013-12-08 02:29:09

Voici un générateur récursif pour les combinaisons (ordre sans importance, sans remplacement):

<?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";
}

Sorties:

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

Même chose sans rendement.

0
répondu sanmai 2016-02-09 09:51:52

A récemment rencontré un problème qui nécessitait des générateurs "récursifs" ou une délégation de générateurs. J'ai fini par écrire une petite fonction qui convertit les appels de générateurs délégués en un seul générateur.

Je l'ai transformé en un paquet afin que vous puissiez simplement l'exiger avec composer, ou commander la source ici: hedronium / generator-nest .

Code:

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--;
        }
    }
}

Vous l'utilisez comme:

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

Commander ce lien ci-dessus. Elle présente des exemples.

0
répondu Omran Jamal 2016-12-22 11:38:24

Réponse courte: les générateurs récursifs sont simples. Exemple de marche à travers l'arbre:

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;
            };
        }
    }
}

C'est tout.

Réponse longue sur fibonacci:

Générateur est quelque chose qui est utilisé avec foreach (generator() as $item) { ... }. Mais OP veut que la fonction fib() retourne int, mais en même temps il veut qu'elle retourne generator pour être utilisée dans foreach. C'est très confus.

Il est possible d'implémenter une solution de générateur récursif pour fibonacci. Nous avons juste besoin de mettre somewere à l'intérieur fib() Fonction une boucle ce sera en effet yield chaque membre de la séquence. Comme generator est censé être utilisé avec foreach, il semble vraiment bizarre, et je ne pense pas que ce soit efficace, mais le voici:

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 . " ";
}
0
répondu cronfy 2017-08-09 12:59:44

Je propose deux solutions pour le nombre de Fibonacci, avec et sans récursivité:

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();
-1
répondu Vladd 2013-11-15 08:34:23