Funciones PHP recursivas anónimas

197

¿Es posible tener una función PHP que sea tanto recursiva como anónima? Este es mi intento de hacerlo funcionar, pero no pasa el nombre de la función.

$factorial = function( $n ) use ( $factorial ) {
    if( $n <= 1 ) return 1;
    return $factorial( $n - 1 ) * $n;
};
print $factorial( 5 );

También soy consciente de que esta es una mala manera de implementar factorial, es solo un ejemplo.

Kendall Hopkins
fuente
No tengo PHP 5.3.0 para verificar pero ¿intentaste usarlo global $factorial?
kennytm
55
(nota al margen) un Lamba es una función anónima, mientras que lo anterior es un Cierre.
Gordon
1
Lambdas y cierres no son mutuamente excluyentes. De hecho, algunas personas creen que un cierre tiene que ser lambda para que sea un cierre (función anónima). Por ejemplo, Python debe darle un nombre a la función primero (según la versión). Debido a que debe darle un nombre que no puede en línea y algunos dirían que lo descalifica para ser un cierre.
Adam Gent
1
print $factorial( 0);
nickb

Respuestas:

356

Para que funcione, debe pasar $ factorial como referencia

$factorial = function( $n ) use ( &$factorial ) {
    if( $n == 1 ) return 1;
    return $factorial( $n - 1 ) * $n;
};
print $factorial( 5 );
Derek H
fuente
eso es raro, los objetos bc siempre deben pasarse por referencia, y anon. las funciones son objetos ...
ellabeauty
25
@ellabeauty en el momento en que se pasa $ factorial, todavía es nulo (no definido), es por eso que debe pasarlo por referencia. Tenga en cuenta que si modifica el factorial $ antes de llamar a la función, el resultado cambiará a medida que pase como referencia.
Marius Balčytis
9
@ellabeauty: No, lo malinterpretas por completo. Todo sin &es por valor. Todo con &es por referencia. Los "objetos" no son valores en PHP5 y no pueden asignarse ni pasarse. Se trata de una variable cuyo valor es una referencia de objeto. Como todas las variables, se puede capturar por valor o por referencia, dependiendo de si hay un &.
newacct
3
Mente soplada! ¡Muchas gracias! ¿Cómo no sabía sobre esto hasta ahora? La cantidad de aplicaciones que tengo para las funciones recursivas anónimas es enorme. Ahora finalmente puedo recorrer estructuras anidadas en diseños sin tener que definir explícitamente un método y mantener todos mis datos de diseño fuera de mis clases.
Dieter Gribnitz
Como dijo @barius, cuidado al usarlo en foreach. $factorialse cambiará antes de llamar a la función y puede resultar en un comportamiento extraño.
Stil
24

Sé que esto podría no ser un enfoque simple, pero aprendí acerca de una técnica llamada "arreglo" de los lenguajes funcionales. La fixfunción de Haskell se conoce más generalmente como el combinador Y , que es uno de los combinadores de punto fijo más conocidos .

Un punto fijo es un valor que no se modifica por una función: un punto fijo de una función f es cualquier x tal que x = f (x). Un combinador de punto fijo y es una función que devuelve un punto fijo para cualquier función f. Como y (f) es un punto fijo de f, tenemos y (f) = f (y (f)).

Esencialmente, el combinador Y crea una nueva función que toma todos los argumentos del original, más un argumento adicional que es la función recursiva. Cómo funciona esto es más obvio usando la notación al curry. En lugar de escribir argumentos entre paréntesis ( f(x,y,...)), escribirlas después de la función: f x y .... El combinador Y se define como Y f = f (Y f); o, con un solo argumento para la función recursed, Y f x = f (Y f) x.

Dado que PHP no curry automáticamente las funciones, es un poco complicado hacer el fixtrabajo, pero creo que es interesante.

function fix( $func )
{
    return function() use ( $func )
    {
        $args = func_get_args();
        array_unshift( $args, fix($func) );
        return call_user_func_array( $func, $args );
    };
}

$factorial = function( $func, $n ) {
    if ( $n == 1 ) return 1;
    return $func( $n - 1 ) * $n;
};
$factorial = fix( $factorial );

print $factorial( 5 );

Tenga en cuenta que esto es casi lo mismo que las soluciones de cierre simples que otros han publicado, pero la función fixcrea el cierre por usted. Los combinadores de punto fijo son ligeramente más complejos que usar un cierre, pero son más generales y tienen otros usos. Si bien el método de cierre es más adecuado para PHP (que no es un lenguaje terriblemente funcional), el problema original es más un ejercicio que para la producción, por lo que el combinador Y es un enfoque viable.

Kendall Hopkins
fuente
10
Vale la pena señalar que call_user_func_array()es tan lento como la Navidad.
Xeoncross
11
@Xeoncross ¿A diferencia del resto de PHP que está estableciendo el récord de velocidad en tierra? : P
Kendall Hopkins
1
Tenga en cuenta que ahora puede (5.6+) usar el desempaquetado de argumentos en lugar de call_user_func_array.
Fabien Sa
@KendallHopkins, ¿por qué estos argumentos adicionales array_unshift( $args, fix($func) );? Args ya está cargado con los parámetros, y la recursión real la realiza call_user_func_array (), entonces, ¿qué hace esa línea?
Quiero respuestas
5

Aunque no es para uso práctico, la extensión de nivel C mpyw-junks / phpext-callee proporciona una recursión anónima sin asignar variables .

<?php

var_dump((function ($n) {
    return $n < 2 ? 1 : $n * callee()($n - 1);
})(5));

// 5! = 5 * 4 * 3 * 2 * 1 = int(120)
mpyw
fuente
0

En las versiones más recientes de PHP puedes hacer esto:

$x = function($depth = 0) {
    if($depth++)
        return;

    $this($depth);
    echo "hi\n";
};
$x = $x->bindTo($x);
$x();

Esto puede conducir a un comportamiento extraño.

jgmjgm
fuente
0

Puede usar Y Combinator en PHP 7.1+ como se muestra a continuación:

function Y
($le)
{return
    (function ($f) 
     {return
        $f($f);
     })(function ($f) use ($le) 
        {return
            $le(function ($x) use ($f) 
                {return
                    $f($f)($x);
                });
        });
}

$le =
function ($factorial)
{return
    function
    ($n) use ($factorial)
    {return
        $n < 2 ? $n
        : $n * $factorial($n - 1);
    };
};

$factorial = Y($le);

echo $factorial(1) . PHP_EOL; // 1
echo $factorial(2) . PHP_EOL; // 2
echo $factorial(5) . PHP_EOL; // 120

Juega con él: https://3v4l.org/7AUn2

Códigos fuente de: https://github.com/whitephp/the-little-phper/blob/master/src/chapter_9.php

Lei Fan
fuente
0

Con una clase anónima (PHP 7+), sin definir una variable:

echo (new class {
    function __invoke($n) {
        return $n < 2 ? 1 : $n * $this($n - 1);
    }
})(5);
Ayell
fuente