Re: [RFC] Lazy Objects

From: Date: Thu, 18 Jul 2024 19:38:39 +0000
Subject: Re: [RFC] Lazy Objects
References: 1 2 3 4 5 6 7 8 9 10 11  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
Hi

On 7/15/24 10:23, Nicolas Grekas wrote:
To me this is what the language evolution should do: Enable users to do things that previously needed to be provided by userland libraries, because they were complicated and fragile, not enabling userland libraries to simplify things that they should not need to provide in the first place because the language already provides it.
That's exactly it: instead of using a third party lib (or in this case implementing a poor man's subset of a correct lazy object implementation), the engine would enable using a native feature to achieve a fully correct behavior in a very simple way. In this case, LazyServiceEntityRepository would directly use ReflectionClass::resetAsLazyGhost to make itself lazy, so that its consumers get a packaged behavior and don't need to care about the topic while consuming the class.
I guess we have to agree to disagree here.
Yes, I think it is clearer. Let me try to rephrase this differently to see if my understanding is correct: --- For every property on that exists on the real instance, the property on the proxy instance effectively [1] is replaced by a property hook like the following:
      public PropertyType $propertyName {
          get {
              return $this->realInstance->propertyName;
          }
          set(PropertyType $value) {
              $this->realInstance->propertyName = $value;
          }
      }
And value that is stored in the property will be freed (including calling the destructor if it was the last reference), as if unset() was called on the property. [1] No actual property hook will be created and the realInstance property does not actually exist, but the semantics behave as if such a hook would be applied.
Conceptually, you've got it right yes!
Sweet. Unless I've missed anything the bit about the value being unset and the destructor implications is missing in the RFC text. It should be added. Also the "Properties that are declared on the real instance are bound to the proxy instance" bit because slightly misleading with the newest change, because the proxy may no longer define additional properties. May I suggest something along the lines of the following: The proxy's properties will be bound to the proxy instance, so that accessing any of these properties on the proxy forwards the operation to the corresponding property on the real instance as if the proxy's property was a virtual property. Any value stored within the proxy's properties will be unset() and the destructor will be called if the proxy held the last reference. This includes properties used with ReflectionProperty::skipLazyInitialization() or setRawValueWithoutLazyInitialization().
Frankly, thinking about this cloning behavior gives me a headache, because it quickly leads to very weird semantics. Consider the following example:
       $predefinedObject = new SomeObj();
       $initializer = function () use ($predefinedObject) {
           return $predefinedObject;
       };
       $myProxy = $r->newLazyProxy($initializer);
       $otherProxy = $r->newLazyProxy($initializer);
       $clonedProxy = clone $myProxy;
       $r->initialize($myProxy);
       $r->initialize($otherProxy);
       $r->initialize($clonedProxy);
To my understanding both $myProxy and $otherProxy would share the $predefinedObject as the real instance and $clonedProxy would have a clone of the $predefinedObject at the time of the initialization as its real instance?
Correct. The sharing is not specifically related to cloning. But when cloning happens, the expected behavior is well defined: we should have separate states.
Yes, it's clear that the should have separate states. The issue I'm having here is that the actual cloning does not happen at the time of the clone operation, but at an arbitrary later point in time and this can have some odd consequences for the object lifecycles. Perhaps my example was too simplified. Let me try to expand the example a little.
    class SomeObj { public string $foo = 'A'; public string $dummy; }
    $predefinedObject = new SomeObj();
    $initializer = function () use ($predefinedObject) {
        return $predefinedObject;
    };
    $myProxy = $r->newLazyProxy($initializer);
    $otherProxy = $r->newLazyProxy($initializer);
    $r->getProperty('foo')->skipLazyInitialization($myProxy);
    $clonedProxy = clone $myProxy;
    var_dump($clonedProxy->foo);
    $r->initialize($myProxy);
    $r->initialize($otherProxy);
    $myProxy->foo = 'B';
    $r->initialize($clonedProxy);
    var_dump($clonedProxy->foo);
I would expect that this dumps 'A' both of the times, because at the time of cloning the $foo property held the the value 'A'. But my understanding is that it returns 'A' at the first time and 'B' at the second time, because $predefinedObject is cloned at the time of the $r->initialize($clonedProxy); call.
To me this sounds like cloning an uninitialized proxy would need to trigger an initialization to result in semantics that do not violate the principle of least astonishment.
Forcing an initialization when cloning would be unexpected. E.g. in Doctrine, when you clone an uninitialized entity, you don't trigger a database roundtrip. Instead, you create a new object that still references the original state internally, but under a different object identity. This cloning behavior is the one we've had for more than 10 years and I think it's also the least astonishing one - at least if we consider this example as a real world trial of this principle.
See above. Best regards Tim Düsterhus

Thread (95 messages)

« previous php.internals (#124497) next »