RFC: lazy statements

php.internals

David Rodrigues

10 years ago
This Feature Request is about the implementation of lazy statements. Basically it should implements a 'lazy' keyword (or similar) and structured like an anonymous function, except that it doesn't accepts arguments, but accept the use() feature. $var = lazy { return 1; } $var = lazy use ($x) { return $x; } It differ from anonymous function because this assignment is treated as variable, without need to call or check if it is a valid Closure all the time. --- Execution example: echo $var; echo $var; In this example, the first echo will execute the lazy statement and set the returned value directly to $var container, then will echo 1. The second echo will not execute the lazy anymore, because it was processed on first echo, so will just echo 1. --- It should be very useful on framework or package development because you can defines lazy properties that need to be calculated only when it is requested the first time. Should be useful too when you have a global variable like $user that you should not use all the time (in all requests). View::share('user', lazy { return User::find(session('user.id')); }); This code will be executed only on try read or write on $user variable. --- It should works from read or write, to the next code is valid: $var = lazy { return [ 1, 2, 3 ]; }; $var[] = 4; --- Currently it is possible do it in a very hackish way: by using magic getter, by create a user getter or a own implementation of this feature. All this ways have a lot of limitations. A hackish example that shows how it should works on current PHP version: http://pastebin.com/cz93wL7n --- I tell more about that here: https://bugs.php.net/bug.php?id=72637
-- David Rodrigues

Larry Garfield

10 years ago
On Thu, Jul 21, 2016, at 10:51 AM, David Rodrigues wrote:
> This Feature Request is about the implementation of lazy statements. > > Basically it should implements a 'lazy' keyword (or similar) and > structured like an anonymous function, except that it doesn't accepts > arguments, but accept the use() feature. > > $var = lazy { return 1; } > $var = lazy use ($x) { return $x; } > > It differ from anonymous function because this assignment is treated > as variable, without need to call or check if it is a valid Closure > all the time. > > --- > > Execution example: > > echo $var; > echo $var; > > In this example, the first echo will execute the lazy statement and > set the returned value directly to $var container, then will echo 1. > The second echo will not execute the lazy anymore, because it was > processed on first echo, so will just echo 1. > > --- > > It should be very useful on framework or package development because > you can defines lazy properties that need to be calculated only when > it is requested the first time. > Should be useful too when you have a global variable like $user that > you should not use all the time (in all requests). > > View::share('user', lazy { return User::find(session('user.id')); }); > > This code will be executed only on try read or write on $user variable. > > --- > > It should works from read or write, to the next code is valid: > > $var = lazy { return [ 1, 2, 3 ]; }; > $var[] = 4; > > --- > > Currently it is possible do it in a very hackish way: by using magic > getter, by create a user getter or a own implementation of this > feature. > All this ways have a lot of limitations. > > A hackish example that shows how it should works on current PHP > version: http://pastebin.com/cz93wL7n > > --- > > I tell more about that here: https://bugs.php.net/bug.php?id=72637 > > -- > David Rodrigues
In concept I like the idea, especially as it would allow for "optionally computed values", which is a performance benefit if those values may not be used. Think of pulling default values out of configuration in the database if not provided at runtime; no sense running the query if it won't be used. I could also see it having very interesting use cases in dependency injection containers. However, that also means there's an enormous potential for race conditions. Technically no more than closures now, but the use cases seem like they'd make it more prevalent. If you "use" by reference, or use a mutable object, or a service that depends on IO, etc. then the resulting value of the lazy variable is not technically non-deterministic, but will certainly feel like it to the developer. Is there a way we could provide better safety around that issue? --Larry Garfield

David Rodrigues

10 years ago
> However, that also means there's an enormous potential for race > conditions. Technically no more than closures now, but the use cases > seem like they'd make it more prevalent. If you "use" by reference, or > use a mutable object, or a service that depends on IO, etc. then the > resulting value of the lazy variable is not technically > non-deterministic, but will certainly feel like it to the developer.
I think that lazy statements should not be used when you can have possibilities like that. It should not be a substitute to methods. So, for instance, if I have a mutable dependency, is better I create a method to do that, that could work better with that. I can think about how we can handle this, but I need a real case where this problem can exists. Can you help with this? Thanks!

Dan Ackroyd

10 years ago
Hi David, On 21 July 2016 at 16:51, David Rodrigues <david.proweb@gmail.com> wrote:
> This Feature Request is about the implementation of lazy statements.
This looks to solve a problem very similar to the one solved by the 'Memoize' annotation in HHVM - https://docs.hhvm.com/hack/attributes/special#__memoize However the memoize functionality seems a lot clearer to me, as it's less 'magic'; it doesn't make accessing a variable actually call a function. Larry Garfield wrote:
>> However, that also means there's an enormous potential for race >> conditions.
I think s/race conditions/circular dependencies/ probably explain the problem more clearly.
> I can think about how we can handle this, but I need a real case > where this problem can exists.
This code has a circular dependency: class Foo { public $a, $b; public __construct() { $this->a = lazy { return 1 + $this->b; } $this->b = lazy { return 1 + $this->a; } } } $foo = new Foo; echo $foo->a; $foo->a can't be resolved until $foo->b is resolved, and $foo->b can't be resolved until $foo->a is resolved. Although that is obvious in a trivial code example, when trying to use a magic feature like this in complex code, where the circular dependency involves, say, 10 items, then it become very hard to reason about the code. Because the 'memoization' of functions is less magic, it is much less prone to this circular dependency problem. cheers Dan

Stas Malyshev

10 years ago
Hi!
> This Feature Request is about the implementation of lazy statements. > > Basically it should implements a 'lazy' keyword (or similar) and > structured like an anonymous function, except that it doesn't accepts > arguments, but accept the use() feature. > > $var = lazy { return 1; }
Is this just keystroke saving for function() or is this supposed to do something additional?
-- Stas Malyshev smalyshev@gmail.com

David Rodrigues

10 years ago
Dan Ackroyd wrote:
> This looks to solve a problem very similar to the one solved by the > 'Memoize' annotation in HHVM - > https://docs.hhvm.com/hack/attributes/special#__memoize
> However the memoize functionality seems a lot clearer to me, as it's > less 'magic'; it doesn't make accessing a variable actually call a > function.
On really it seems a very good feature focused on method call. Maybe it can be done in same idea, by freezing the first result of method. But I think hard do that with args. Maybe something like that: http://pastebin.com/YKjEYeYF Note: I know that is better an IoC, understand just as example. Dan Ackroyd wrote:
> This code has a circular dependency: > > class Foo { > public $a, $b; > public __construct() { > $this->a = lazy { return 1 + $this->b; } > $this->b = lazy { return 1 + $this->a; } > } > } > > $foo = new Foo; > echo $foo->a; > > $foo->a can't be resolved until $foo->b is resolved, and $foo->b can't > be resolved until $foo->a is resolved.
Now back to topic, this circular dependency too occur with functions. Basically if it happen, should throw a infinite recursion error. (Currently I guess that PHP don't do that, but xdebug does) In all case, $a should not depends of $b, when $b depends $a. In this case, should have a bootstrap system, that turn it independent. Example: class Foo { public $a, $b; public __construct () { $c = lazy { return generalDependency(); } $this->a = lazy use ($c) { return 1 + $c; }; $this->b = lazy use ($c) { return 1 + $c; }; } } Dan Ackroyd wrote:
> Because the 'memoization' of functions is less magic, it is much less > prone to this circular dependency problem.
I guess that memoization is another topic, but still very interesting, as cited above. So maybe it can be discussed apart of this email, if is reasonable. Stanislav Malyshev wrote:
> > This Feature Request is about the implementation of lazy statements. > > > > Basically it should implements a 'lazy' keyword (or similar) and > > structured like an anonymous function, except that it doesn't accepts > > arguments, but accept the use() feature. > > > > $var = lazy { return 1; } > > Is this just keystroke saving for function() or is this supposed to do > something additional?
It's similar to function (), but should be applied to variables or properties only. But exists an important difference between you use lazy vs function (). $var1 = function () { sleep(5); return 1; } $var2 = lazy { sleep(5); return 1; } - $var1 should be called like $var1(); - If I call $var1() five times, it'll execute this structure over 25 seconds; - If I try to write over $var1(), possible I should get an error trying to write over int (1); - $var2 should be called like $var2 directly; - If I call $var2 five times, it'll execute once the structure, custing 5 seconds only; - If I try to write over $var2 it'll, in fact, write over $var2 processed structure, that stores int (1);

Dan Ackroyd

10 years ago
On 24 July 2016 at 06:14, David Rodrigues <david.proweb@gmail.com> wrote:
> Now back to topic, this circular dependency too occur with functions.
Yes, but the difference is in the surprise factor. Two functions having a circular dependency ==> not too astonishing, and easy to think about. Reading the value of a variable having a circular dependency ==> give up programming to become a farmer. cheers Dan https://en.wikipedia.org/wiki/Principle_of_least_astonishment

David Rodrigues

10 years ago
Dan Ackroyd wrote:
> Yes, but the difference is in the surprise factor. > > Two functions having a circular dependency ==> not too astonishing, > and easy to think about. > > Reading the value of a variable having a circular dependency ==> give > up programming to become a farmer.
Okay, so we can avoid circular dependencies (in sense of find where a circular dependency occur) by creating a progression system over lazy objects. Something like: Lazy { progress { DEFINED, EXECUTING, FINISHED }; statement } For instance, when you defines a lazy statement, it states is set as "defined". When you starts execution over it, it changes the states to "executing". After it finish, it change states to "finished". This last state is the final value of variable, basically. So, in case of a lazy statement be called on "executing" phase, it should throw an error. For instance: $b = null; $a = lazy use ($b) { return $b; } $b = lazy use ($a) { return $a; } In this case, both $a and $b are on state "defined". echo $a; When I call $a now, it set states to "executing". In this case, it'll try read $b that is lazy too. $b is now "executing" too. Now $b will try to read $a, but $a already is "executing" So PHP should throw an error like "cyclic execution on line x". But in all way, lazy statements should not be used on all places I should not substitute default variable, mainly because it should be less performatic (not too much, I guess). Lazy statements should be used only in case where the variable contents is very complex (high cost) and very few useful in all requests. And where methods or functions is not too viable. Example: class Foo { public $something = lazy { // do complex things. return $complexValue; }; } $foo = new Foo; $foo->something++; Instead of: class Foo { private $something; public function getSomething() { if (!$this->something) { // do complex things. $this->something = $complexValue; } return $this->something; } public function setSomething($value) { $this->something = $value; } } $foo = new Foo; $fooSomething = $foo->getSomething(); $fooSomething++; $foo->setSomething($fooSomething);