rfc:closures:object-extension

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
rfc:closures:object-extension [2009/01/22 14:07] – created cseilerrfc:closures:object-extension [2017/09/22 13:28] (current) – external edit 127.0.0.1
Line 1: Line 1:
 ====== Closures: Object extension ====== ====== Closures: Object extension ======
 +  * Date: 2009-01-22
 +  * Author: Unknown
 +  * Status: Implemented in PHP 5.4
  
-Recently, efforts were made to go beyond the scope of the original Closures proposal ([[rfc/closures]]) in order to allow the extension of objects dynamically, take the following example code:+===== Introduction ===== 
 + 
 +Efforts were made to go beyond the scope of the original Closures proposal ([[rfc/closures]]) in order to allow the extension of objects dynamically, take the following example code:
  
 <code php> <code php>
Line 41: Line 46:
  
 Please: Before reading, keep in mind that the definition of "closure" is simply an object then saves the scope that there is at creation time. Since closures come from functional programming (i.e. lambda calculus), the way closures and OOP interact is up to the programming language that combines both. Different programming languages that implement both have different approaches to this topic that are tailored to the specific language. Please: Before reading, keep in mind that the definition of "closure" is simply an object then saves the scope that there is at creation time. Since closures come from functional programming (i.e. lambda calculus), the way closures and OOP interact is up to the programming language that combines both. Different programming languages that implement both have different approaches to this topic that are tailored to the specific language.
 +
 +===== History =====
 +
 +For PHP 5.3 $this support for Closures was removed because no consensus could be reached how to implement it in a sane fashion. This RFC describes the possible roads that can be taken to implement it in the next PHP version.
  
 ===== Approaches to $this binding ===== ===== Approaches to $this binding =====
 +
 +==== (0) Proposal: Keep as-is, no $this support in closures ====
 +
 +In PHP 5.3 $this is not supported in closures - the simplest solution is to ensure HEAD (PHP 6.0) exhibits the same behaviour as 5.3 and make lamdas/closures a purely functional and non-OOP tool.
  
 ==== (A) Original proposal: Bind $this to the object scope at creation (if existing) ==== ==== (A) Original proposal: Bind $this to the object scope at creation (if existing) ====
Line 136: Line 149:
   | 0 | 1 | 2 | 3 | 4 | 5 | 6   | 0 | 1 | 2 | 3 | 4 | 5 | 6
 ==+===+===+===+===+===+===+=== ==+===+===+===+===+===+===+===
 +0 | # | # | - | # | - | # | #
 +--+---+---+---+---+---+---+---
 A | 1 | 1 | - | 1 | - | 1 | 1 A | 1 | 1 | - | 1 | - | 1 | 1
 --+---+---+---+---+---+---+--- --+---+---+---+---+---+---+---
Line 272: Line 287:
    - Try to find a property in the property table with the name and if it is a closure, call the closure.    - Try to find a property in the property table with the name and if it is a closure, call the closure.
    - Use __call    - Use __call
-   No not use __get+   Do not use __get
  
 **NOTE** This appears to be the conensus on this issue as far as I can see on-list. If this should be not the case, please correct this section. **NOTE** This appears to be the conensus on this issue as far as I can see on-list. If this should be not the case, please correct this section.
Line 336: Line 351:
   - Always use the class scope of the object that bindTo() is called for   - Always use the class scope of the object that bindTo() is called for
   - Always use the class scope of the function that calls bindTo()   - Always use the class scope of the function that calls bindTo()
 +  - Add a parameter to bindTo() to specify this.
 +  - Add some syntax to the closure definition to specify this.
 +  - Always use a dummy class scope so only public properties are accessible.
  
 This is still to be discussed. This is still to be discussed.
Line 345: Line 363:
 ==== Object cloning ==== ==== Object cloning ====
  
-When an object containing closure methods is cloned, the closure methods should **not** be automatically rebound but rather should the object have to do it itself in the __clone method. With this behaviour, the user has the control over whether to rebind the closures on cloning or not.+When an object containing closure methods is cloned, the closure methods should **not** be automatically rebound but rather should the object have to do it itself in the %%__clone%% method. With this behaviour, the user has the control over whether to rebind the closures on cloning or not.
  
 This may still be subject to discussion. This may still be subject to discussion.
  
-==== Important: Timeline ====+===== Status as of August 10 2010 =====
  
-In order to be able to proceed to PHP 5.3 beta 1 quickly, I suggest the following timeline:+In April 2010 was [[http://svn.php.net/viewvc?view=revision&revision=298187|committed]] a subset of modified proposal A.
  
-  - For beta1, we make sure that proposal A (no closure-property-calling, no rebinding) is in CVS. +==== Calling methods ====
-  - post-beta1, we add those features to PHP after the details that are not entirely clear are discussed. If the discussion takes too long, we don't include that feature in PHP 5.3 but instead we wait for the next version (5.3.1 or 5.4 or 6.0 or whatever)+
  
-Important observation: If we have closure-property-calling but we DO NOT have rebinding, then this will certainly cause confusion among users coming from JavascriptThus we should consider them a unit and only add them both or none at all.+An implementation of this feature has not been commitedOne cannot call closures stored in properties as if they were methods.
  
-===== Alternative, if no consensus can be reached =====+This [[http://nebm.ist.utl.pt/~glopes/misc/closures_as_methods.patch|patch (with tests)]] implements support for this feature.
  
-If no consensus regarding this can be reachedwe could revert closures to be non-OOP-aware objects, i.e. that $this is **never** available inside closures. This would be a major step back in my eyesbut it would at least leave it open for discussion how to actually implement $this without breaking BC while being able to release PHP 5.3 without that support.+The following two graphs explain which combinations are allowed of staticness of method callsproperties where the closures are stored and the closures themselves, as implemented in the patch (green means "allowed with no error", yellow means "E_STRICT error"orange means "E_WARNING error" and red means "fatal error").
  
 +{{:rfc:closures:clos-non-static-call.png|}}
 +
 +{{:rfc:closures:clos-static-call.png|}}
 +
 +Other issues:
 +
 +  - Do we really want to make closures-as-methods have priority over ''%%__call%%''/''%%__callStatic%%''? On one hand, there's no way to otherwise allow closures-as-methods in classes that implement these magic methods, on the other one, this breaks BC and makes the magic methods even more inefficient – we have to check if there is a method **and** a property with that name.
 +  - Properties are not case-sensitive, hence calling closures-as-methods is case-sensitive (contrary to calling regular methods).
 +  - What to do with properties with no visibility? Ignore them (and let fall back to ''%%__call%%''/''%%__callStatic%%'') or raise error (the implementation in the patch raises an error).
 +  - What to do with properties used as methods that are not closures? Ignore them or raise an error (the implementation raises an error).
 +  - Should we throw an exception when calling a closure-as-instance method that's stored as a static method? Usually, accessing a static property in a non static context raises an E_STRICT, but I think may very well be very useful, because we can swap instance method implementations on a class basis instead of only an instance basis.
 +
 +Note that, contrary to what the proposal says, this will not work at all (fatal error, not warning):
 +
 +<code php>
 +// $obj is not $this
 +$obj->method1 = function () { ... };
 +//this is a method call, we need an instance or a static closure!
 +$obj->method1 (); //PHP Fatal error:  Non-static closure called as method but not bound to any object
 +//only if the closure is actually bound to some other object (as opposed to not be bound at all), will we get_
 +// WARNING: Closure called as method but bound object differs from containing object.
 +</code>
 +
 +==== Private/protected members (scope) ====
 +
 +The currently implemented handling of scope for class closures is:
 +
 +  - They initially inherit the (calling) scope of the class they were created in.
 +  - After that, always use the class scope of the object that bindTo() is called for ([[#private_protected_member_access|option 2]])
 +  - If there's any bound instance, the called scope is set to the class of it, otherwise it's the same as the calling scope.
 +
 +The implementation of option #2 has serious drawbacks. Consider the following code:
 +
 +<code php>
 +class foo {
 + private $field = "foo";
 + function getClosure() {
 + return function () {
 + echo $this->field, "\n";
 + };
 + }
 +}
 +class subFoo extends foo {}
 + 
 +$f = new subFoo();
 +$g = new subFoo();
 +$c = $f->getClosure();
 +$c(); //foo
 +$c = $c->bindTo($g); //or even $c->bindTo($f)
 +$c(); //fails
 +</code>
 +
 +Since it's always taking the class of the bound object as scope, this means we have no way to keep the original scope of the closure without binding an instance of exactly the same class. It's against the basic principles of OOP to have something that works when passed A, but not when passed a subclass of A.
 +
 +There's always another problem. The current implementation allows changing the scope of a static closure, but only by attempting to bind it with am object of the desired class. This has two problems: it requires an instance for a solely static operation and it may even not be possible to generate objects of the desired class (e.g. it's abstract).
 +
 +Therefore, I propose an implementation ([[http://nebm.ist.utl.pt/~glopes/misc/closures_scope.patch|patch here]]) of option #4: "Add a parameter to bindTo() to specify this." This option is the most versatile and the implementation in the patch has very little magic – it only changes the scope of the closure if it's requested, otherwise it keeps the previous scope. The order of the current arguments of ''Closure::bind'' was also changed so that its implementation could be unified to that of ''Closure::bindTo'' using ''zend_parse_method_parameters''. The prototype is this:
 +
 +<code>
 +Closure Closure::bind(Closure $old, object $to [, mixed $scope = "static" ] )
 +</code>
 +
 +If the last argument is not given, or if "static" is specified, the current scope (or lack thereof) is preserved, with one exception noted below.
 +
 +The patch preserves these invariants:
 +  - A static closure, being scoped or not, cannot have any bound instance.
 +  - A non static closure has a bound instance iif it is scoped.
 +
 +To preserve these invariants, there are these additional rules:
 +  - If a non static closure is given a scope (or it already has a scope, but the scope parameter is not specified) and a NULL instance, it's made static.
 +  - If a static closure is given an instance, the instance is ignored and an ''E_WARNING'' notice is emitted.
 +  - If a non static non scoped (and therefore non bound) instance is given no scope and a non NULL instance, it's given a dummy scope (currently the "Closure" scope). **This is the only case where the previous scope, or lack thereof, is changed by the rebinding process**. This is necessary because it's not legal to have ''$this'' with a NULL scope.
 +
 +Example:
 +
 +<code php>
 +class A {
 + private $x;
 +
 + public function __construct($v) {
 + $this->x = $v;
 + }
 +
 + public function getIncrementor() {
 + return function() { return ++$this->x; };
 + }
 +}
 +class B extends A {
 + private $x;
 + public function __construct($v) {
 + parent::__construct($v);
 + $this->x = $v*2;
 + }
 +}
 +
 +$a = new A(0);
 +$b = new B(10);
 +
 +$ca = $a->getIncrementor();
 +var_dump($ca()); //int(1)
 +
 +echo "Testing with scope given as object", "\n";
 +
 +$cb = $ca->bindTo($b, $b);
 +$cb2 = Closure::bind($ca, $b, $b);
 +var_dump($cb()); //int(21)
 +var_dump($cb2()); //int(22)
 +
 +echo "Testing with scope as string", "\n";
 +
 +$cb = $ca->bindTo($b, 'B');
 +$cb2 = Closure::bind($ca, $b, 'B');
 +var_dump($cb()); //int(23)
 +var_dump($cb2()); //int(24)
 +
 +$cb = $ca->bindTo($b, NULL);
 +var_dump($cb()); //Fatal error: Cannot access private property B::$x
 +</code>
rfc/closures/object-extension.1232633259.txt.gz · Last modified: 2017/09/22 13:28 (external edit)