views:

832

answers:

9

In JavaScript, you can use Lazy Function Definitions to optimize the 2nd - Nth call to a function by performing the expensive one-time operations only on the first call to the function.

I'd like to do the same sort of thing in PHP 5, but redefining a function is not allowed, nor is overloading a function.

Effectively what I'd like to do is like the following, only optimized so the 2nd - Nth calls (say 25-100) don't need to re-check if they are the first call.

$called = false;
function foo($param_1){
  global $called;
  if($called == false){
    doExpensiveStuff($param_1);
    $called = true;
  }
  echo '<b>'.$param_1.'</b>';
}

PS I've thought about using an include_once() or require_once() as the first line in the function to execute the external code just once, but I've heard that these too are expensive.

Any Ideas? or is there a better way to tackle this?

+6  A: 

Have you actually profiled this code? I'm doubtful that an extra boolean test is going to have any measurable impact on page rendering time.

John Millikin
The code is memoizing. the bool test ( although probably easier to write as if( !$called ) ) decides whether or not to execute the slow code so it only runs once.
Kent Fredric
@Kent: read scunliffe's link. He's trying to redefine the function after the first time it's run so he can skip the `if($called)` test.
John Millikin
+4  A: 

you can do conditional function definiton.

if( !function_exists('baz') )
{ 
    function baz( $args ){ 
        echo $args; 
    }
}

But at present, a function becomes a brick when defined.

You can use create_function, but I would suggest you DONT because it is slow, uses lots of memory, doesn't get free()'d untill php exits, and is a security hole as big as eval().

Wait till PHP5.3, where we have "closures" http://wiki.php.net/rfc/closures

Then you'll be permitted to do

if( !isset( $baz ) ) 
 { 
    $baz = function( $args )
    { 
        echo $args;
    }
}

$baz('hello');

$baz = function( $args )
{ 
       echo $args + "world"; 
}
$baz('hello');

Upon further reading, this is the effect you want.

$fname = 'f_first'; 
function f_first( $even ) 
{ 
    global $fname; 
    doExpensiveStuff(); 
    $fname = 'f_others';
    $fname( $even );
    /* code */ 
}
function f_others( $odd ) 
{
     print "<b>".$odd."</b>";
}

foreach( $blah as $i=>$v ) 
{
   $fname($v);
}

It'll do what you want, but the call might be a bit more expensive than a normal function call.

In PHP5.3 This should be valid too:

$func = function( $x ) use ( $func ) 
{ 
     doexpensive(); 
     $func = function( $y )
     { 
          print "<b>".$y."</b>";
     }
     $func($x);
}
foreach( range(1..200) as $i=>$v ) 
{ 
    $func( $v ); 
}

( Personally, I think of course that all these neat tricks are going to be epically slower than your earlier comparison of 2 positive bits. ;) )

If you're really concerned about getting the best speed everywhere

$data = // some array structure
doslowthing(); 
foreach( $data as $i => $v ) 
{
   // code here 
}

You may not be able to do that however, but you've not given enough scope to clarify. If you can do that however, then well, simple answers are often the best :)

Kent Fredric
That's a complex, convoluted solution that isn't even available to use yet. A local static var is the simple solution.It's also relatively easy to cache it outside the script, depending on how much work had to be done to process it initially.
Alister Bulman
A: 

If you do wind up finding that an extra boolean test is going to be too expensive, you can set a variable to the name of a function and call it:

$func = "foo";    

function foo()
{
    global $func;
    $func = "bar";
    echo "expensive stuff";
};


function bar()
{
    echo "do nothing, i guess";
};

for($i=0; $i<5; $i++)
{
    $func();
}

Give that a shot

Jurassic_C
Hmm, this looks like it might work, definitely one to profile, thanks.
scunliffe
But it's still a function call, which is probably quite expensive by itself.
Christian Davén
A: 

Any reason you're commited to a functional style pattern? Despite having anonymous functions and plans for closure, PHP really isn't a functional language. It seems like a class and object would be the better solution here.

Class SomeClass{
    protected $whatever_called;
    function __construct(){
        $this->called = false;
    }
    public function whatever(){
        if(!$this->whatever_called){
            //expensive stuff
            $this->whatever_called = true;
        }
        //rest of the function
    }
}

If you wanted to get fancy you could use the magic methods to avoid having to predefine the called booleans. If you don't want to instantiate an object, go static.

Alan Storm
PHP really isn't a funtional language.FWIW, Classes are a relatively "new" thing to php. Until php5 they were hardly even classes. Classes also have overhead, and you're code is logically identical to the original.
Kent Fredric
Yeah, all true, but the direction PHP has been headed in the past two/three years is towards a classical concept of OOP, and the functional abstractions in PHP come with their own level of overhead. Just another option to consider.
Alan Storm
A: 

Please don't use include() or include_once(), unless you don't care if the include() fails. If you're including code, then you care. Always use require_once().

Andy Lester
A: 

I was hoping someone had a magic answer for this. ;-) All efforts appreciated and there are a bunch of good ideas here. I think I'm going to need to build some full tests, and profile them to determine the best action. @Andy yeah, the require_once() would be more applicable/safe than the include_once(), but I'm guessing the original boolean test would still be less expensive?

I'll post any findings I get here shortly.

scunliffe
"original boolean test would still be less expensive?" the word "global" would be my speed concern.
Kent Fredric
+10  A: 

Use a local static var:

function foo() {
    static $called = false;
    if ($called == false) {
        $called = true;
        expensive_stuff();
    }
}

Avoid using a global for this. It clutters the global namespace and makes the function less encapsulated. If other places besides the innards of the function need to know if it's been called, then it'd be worth it to put this function inside a class like Alan Storm indicated.

dirtside
A: 

PHP doesn't have lexical scope, so you can't do what you want with a function. However, PHP has classes, which conceptually works in exactly the same way for this purpose.

In javascript, you would do:

var cache = null;
function doStuff() {
  if (cache == null) {
    cache = doExpensiveStuff();
  }
  return cache;
}

With classes (In PHP), you would do:

class StuffDoer {
  function doStuff() {
    if ($this->cache == null) {
      $this->cache = $this->doExpensiveStuff();
    }
    return $this->cache;
  }
}

Yes, class-based oop is more verbose than functional programming, but performance-wise they should be about similar.

All that aside, PHP 5.3 will probably get lexical scope/closure support, so when that comes out you can write in a more fluent functional-programming style. See the PHP rfc-wiki for a detailed description of this feature.

troelskn
A: 

How about using local static variables?

function doStuff($param1) {
    static $called = false;
    if (!$called) {
        doExpensiveStuff($param1);
        $called = true;
    }
    // do the rest
}

If you need to do expensive stuff only once for given parameter value, you could use an array buffer:

function doStuff($param1) {
    static $buffer = array();
    if (!array_key_exists($param1, $buffer)) {
        doExpensiveStuff($param1);
        $buffer[$param1] = true;
    }
    // do the rest
}

Local static variables are persistent across function calls. They remember the value after return.

Michał Rudnicki