php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Request #50980 lambda/anonymous functions do not have proper scope
Submitted: 2010-02-09 22:53 UTC Modified: 2010-11-24 09:45 UTC
Votes:2
Avg. Score:3.0 ± 0.0
Reproduced:1 of 2 (50.0%)
Same Version:1 (100.0%)
Same OS:1 (100.0%)
From: nate at frickenate dot com Assigned:
Status: Not a bug Package: Scripting Engine problem
PHP Version: 5.3.1 OS: Linux
Private report: No CVE-ID:
 [2010-02-09 22:53 UTC] nate at frickenate dot com
Description:
------------
The way in which variables are passed to the inside of a lambda function via the 'use (...)' syntax is broken. The entire concept of "scope" or "closures" does not seem to apply in php whatsoever.

Variables within a lambda are supposed to be resolved/looked up in the parent scope at runtime when the variable is actually accessed within the lambda function's code. It seems that php just dumbly creates a copy of the variable when it sees the definition of the lambda function with the 'use (...)' clause. Changes to the variable in the parent scope between the moment where the lambda is defined and the lambda function is called are not propagated.

The worst part happens with objects. When passing in an object instance to a lambda, any changes to the original object in the parent scope after the lambda definition do affect the same object (problem 4 in reproduce code), but replacing the object altogether does not (problem 3 in reproduce code). I imagine this has to do with the php5 "fake references" that are used to pass objects around.

I get the feeling this is just a horrible limitation of the devs having hacked a "best-possible" implementation of "lambdas" into a Zend codebase that can't handle proper scope lookup. If this is the case, it would be nice if the documentation could be updated to really nail home the fact that there is no scoping going on at all, and that quite literally all you get is a copy of the variable as it exists at the moment the lambda is defined (except for the strangeness going on with objects).

It's unfortunate that the "solution" for the time being is to *always* pass in *every* variable as a reference, which results in expected output. But this requires creating a local copy within the lambda for every variable that one wants to modify without affecting the parent scope. :'(

Reproduce code:
---------------
<?php

// problem 1: this should echo "Canada", not a php notice
$fn = function () use ($country) { echo $country . "\n"; };
$country = 'Canada';
$fn();


// problem 2: this should echo "Canada", not "UnitedStates"
$country = 'UnitedStates';
$fn = function () use ($country) { echo $country . "\n"; };
$country = 'Canada';
$fn();


// problem 3: this should echo "Canada", not "UnitedStates"
$country = (object)array('name' => 'UnitedStates');
$fn = function () use ($country) { echo $country->name . "\n"; };
$country = (object)array('name' => 'Canada');
$fn();


// problem 4: this outputs "Canada". if this outputs "Canada",
// then so should problem 2 above. otherwise this should be
// just as broken as problem 2 and be outputting "UnitedStates"
$country = (object)array('name' => 'UnitedStates');
$fn = function () use ($country) { echo $country->name . "\n"; };
$country->name = 'Canada';
$fn();

?>

Expected result:
----------------
If scope was actually handled properly, then the lookup of the "use (...)"'d variable would occur at the moment the variable is used within the lambda, resulting in the following output:

Canada
Canada
Canada
Canada

Actual result:
--------------
PHP Notice:  Undefined variable: country in ... on line 5

UnitedStates
UnitedStates
Canada

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2010-05-21 00:35 UTC] a at b dot c dot de
use() parameters are early binding - they use the variable's value at the point where the lambda function is declared, rather than the point where the lambda function is called (late binding).

If you want late binding then there is already syntax for that ('&') - which works as expected to propagate assignments up to the calling scope. To make it the default would be (a) contrary to the way "ordinary" function parameters are passed, (b) require the addition of new syntax to indicate that early binding is wanted, (c) deal with the situation where a used variable doesn't exist at call time, (d) make it hard to understand the difference between an ordinary function parameter and a closure.

The problem with allowing variables in the outer scope to leak by default into the lambda function is that a copy of the ENTIRE collection returned by get_defined_vars() would need to be persisted for the lifetime of the closure object (less any that are overridden by call-time arguments) just in case any of them are needed.

'I imagine this has to do with the php5 "fake references" that are used to pass objects around.'
The mechanism is the same as that used in Java and C#: the object reference is passed by value. It's just that in PHP you also have the option of passing the reference by reference as well (see the chapter in the manual on "Objects and references", where it explains that what is passed is an "object identifier" and not a PHP "reference").
 [2010-08-28 14:58 UTC] + at ni-po dot com
function () use (&$country)

will fix it (as the previous comment already says). This is in concordance to the handling of arguments in normal functions and thus mustn't be changed.
 [2010-11-24 09:45 UTC] jani@php.net
-Status: Open +Status: Bogus -Package: Feature/Change Request +Package: *General Issues
 [2010-11-24 09:45 UTC] jani@php.net
see the last comments..
 [2010-11-24 09:45 UTC] jani@php.net
-Package: *General Issues +Package: Scripting Engine problem
 [2011-12-07 15:49 UTC] paulomiguelmarques at gmail dot com
I guess it makes sense to be this way to be consistent with the rest of PHP, but it should definitely be properly documented on the web site, which it isn't.
 
PHP Copyright © 2001-2014 The PHP Group
All rights reserved.
Last updated: Thu Apr 24 02:02:10 2014 UTC