238

Is it possible to have a PHP function that is both recursive and anonymous? This is my attempt to get it to work, but it doesn't pass in the function name.

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

I'm also aware that this is a bad way to implement factorial, it's just an example.

Wil Moore III
  • 6,968
  • 3
  • 36
  • 49
Kendall Hopkins
  • 43,213
  • 17
  • 66
  • 89
  • I don't have PHP 5.3.0 to check but did you try using `global $factorial`? – kennytm Mar 19 '10 at 20:02
  • 5
    *(sidenote)* a Lamba is an anonymous function, while the above is a Closure. – Gordon Mar 19 '10 at 20:21
  • 1
    Lambdas and Closures are not mutually exclusive. In fact some people believe that a closure has to be lambda for it to be a closure (anonymous function). For example Python you sort of have to give the function a name first (depending on version). Because you have to give it a name you can't inline and some would say that disqualifies it from being a closure. – Adam Gent May 06 '10 at 13:15
  • 1
    `print $factorial( 0);` – nickb Apr 20 '13 at 04:14
  • php Manual [example](http://php.net/manual/en/functions.anonymous.php#94313) –  Jul 21 '17 at 03:50

6 Answers6

448

In order for it to work, you need to pass $factorial as a reference

$factorial = function( $n ) use ( &$factorial ) {
    if( $n == 1 ) return 1;
    return $factorial( $n - 1 ) * $n;
};
print $factorial( 5 );
Elle H
  • 11,837
  • 7
  • 39
  • 42
  • that's weird bc objects should always be passed by reference, and anon. functions are objects... – ellabeauty Aug 14 '12 at 21:31
  • 33
    @ellabeauty in the time $factorial is passed, it is still null (not defined), that's why you have to pass it by reference. Be aware, that if you modify the $factorial before calling the function, the result will change as it's passed by reference. – Marius Balčytis Sep 13 '12 at 21:48
  • 9
    @ellabeauty: No, you completely misunderstand it. Everything without `&` is by value. Everything with `&` is by reference. "Objects" are not values in PHP5 and cannot be assigned or passed. You are dealing with a variable whose value is an object reference. Like all variables, it can be captured by value or by reference, depending on whether there is an `&`. – newacct Apr 20 '13 at 10:05
  • 3
    Mind Blown! Thanks a lot! How did I not know about this till now? The amount of application I have for recursive anonymous functions is huge. Now I can finally loop through nested structures in layouts without having to explicitly define a method and keep all my layout data out of my classes. – Dieter Gribnitz Feb 26 '14 at 10:14
  • Like @barius said, careful when using it in foreach. `$factorial` will be changed before function was called and it may result with strange behaviour. – stil May 15 '15 at 23:13
27

I know this might not be a simple approach, but I learned about a technique called "fix" from functional languages. The fix function from Haskell is known more generally as the Y combinator, which is one of the most well-known fixed point combinators.

A fixed point is a value that is unchanged by a function: a fixed point of a function f is any x such that x = f(x). A fixed point combinator y is a function that returns a fixed point for any function f. Since y(f) is a fixed point of f, we have y(f) = f(y(f)).

Essentially, the Y combinator creates a new function that takes all the arguments of the original, plus an additional argument that's the recursive function. How this works is more obvious using curried notation. Instead of writing arguments in parentheses (f(x,y,...)), write them after the function: f x y .... The Y combinator is defined as Y f = f (Y f); or, with a single argument for the recursed function, Y f x = f (Y f) x.

Since PHP doesn't automatically curry functions, it's a bit of a hack to make fix work, but I think it's interesting.

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

Note this is almost the same as the simple closure solutions others have posted, but the function fix creates the closure for you. Fixed point combinators are slightly more complex than using a closure, but are more general, and have other uses. While the closure method is more suitable for PHP (which isn't a terribly functional language), the original problem is more of an exercise than for production, so the Y combinator is a viable approach.

outis
  • 75,655
  • 22
  • 151
  • 221
Kendall Hopkins
  • 43,213
  • 17
  • 66
  • 89
  • 12
    It's worth noting that `call_user_func_array()` is slow as Christmas. – Xeoncross May 09 '11 at 21:02
  • 13
    @Xeoncross As opposed to the rest of PHP which is setting the land speed record? :P – Kendall Hopkins May 09 '11 at 21:12
  • 1
    Note, you can now (5.6+) use argument unpacking instead of `call_user_func_array`. – Fabien Sa Apr 17 '17 at 15:43
  • @KendallHopkins why this additional arguments `array_unshift( $args, fix($func) );`? Args is already laden with the parameters, and the actual recursion is done by the call_user_func_array(), so what does that line do? – I Want Answers Aug 25 '18 at 08:27
7

Although it is not for practial usage, The C-level extension mpyw-junks/phpext-callee provides anonymous recursion without assigning variables.

<?php

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

// 5! = 5 * 4 * 3 * 2 * 1 = int(120)
mpyw
  • 5,526
  • 4
  • 30
  • 36
3

With an anonymous class (PHP 7+), without defining a variable:

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

In newer versions of PHP you can do this:

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

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

This can potentially lead to strange behaviour.

jgmjgm
  • 4,240
  • 1
  • 25
  • 18
-1

You can use Y Combinator in PHP 7.1+ as below:

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

Play with it: https://3v4l.org/7AUn2

Source codes from: https://github.com/whitephp/the-little-phper/blob/master/src/chapter_9.php

Lei Fan
  • 9
  • 1