Re: [low priority] WeakMaps with scalar keys

From: Date: Thu, 11 Jul 2024 17:29:45 +0000
Subject: Re: [low priority] WeakMaps with scalar keys
References: 1 2 3 4 5  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
On Thu, Jul 11, 2024, at 08:45, Rob Landers wrote:
> 
> 
> On Thu, Jul 11, 2024, at 01:11, Benjamin Morel wrote:
>>> The answer is: it depends. If you don’t need the array to clean up after itself, you
>>> can indeed use an array of WeakReference to get most of the way there. If you want it to clean up
>>> after an object gets removed, you either need to add support to the stored object’s destructor
>>> (which isn’t always possible for built-in or final types), or create your own garbage collector
>>> that scans the array. 
>> 
>> It is indeed doable in userland using WeakReferences, with a small performance penalty:
>> 
>> ```
>> class ReverseWeakMap implements Countable, IteratorAggregate, ArrayAccess
>> {
>>     /**
>>      * @var array<int|string, WeakReference>
>>      */
>>     private array $map = [];
>> 
>>     public function count(): int
>>     {
>>         foreach ($this->map as $value => $weakReference) {
>>             if ($weakReference->get() === null) {
>>                 unset($this->map[$value]);
>>             }
>>         }
>> 
>>         return count($this->map);
>>     }
>> 
>>     public function getIterator(): Generator
>>     {
>>         foreach ($this->map as $value => $weakReference) {
>>             $object = $weakReference->get();
>> 
>>             if ($object === null) {
>>                 unset($this->map[$value]);
>>             } else {
>>                 yield $value => $object;
>>             }
>>         }
>>     }
>> 
>>     public function offsetExists(mixed $offset)
>>     {
>>         if (isset($this->map[$offset])) {
>>             $object = $this->map[$offset]->get();
>> 
>>             if ($object !== null) {
>>                 return true;
>>             }
>> 
>>             unset($this->map[$offset]);
>>         }
>> 
>>         return false;
>>     }
>> 
>>     public function offsetGet(mixed $offset): object
>>     {
>>         if (isset($this->map[$offset])) {
>>             $object = $this->map[$offset]->get();
>> 
>>             if ($object !== null) {
>>                 return $object;
>>             }
>> 
>>             unset($this->map[$offset]);
>>         }
>> 
>>         throw new Exception('Undefined offset');
>>     }
>> 
>>     public function offsetSet(mixed $offset, mixed $value): void
>>     {
>>         $this->map[$offset] = WeakReference::create($value);
>>     }
>> 
>>     public function offsetUnset(mixed $offset): void
>>     {
>>         unset($this->map[$offset]);
>>     }
>> }
>> ``` 
>> 
>>> Now that I think about it, it might be simpler to add an “onRemove()” method that
>>> takes a callback for the WeakReference class. 
>>> 
>>> — Rob
>> 
>> 
>> A callback when an object goes out of scope would be a great addition to both WeakReference
>> & WeakMap indeed, it would allow custom userland weak maps like the above, with next to no
>> performance penalty!
>> 
>> - Benjamin
>>  
> 
> The callback is surprisingly easy to implement, at least for WeakReference (did it in about 10
> minutes on the train as a hack). I haven’t looked into WeakMap yet, but I suspect much of the
> plumbing is the same. 
> 
> I also looked into the ReverseWeakMap a bit and it seems there are just too many foorguns to
> make it worthwhile. For example:
> 
> $reverseWeakMap[$key] = new Obj();
> 
> is actually a noop as in it does absolutely nothing. It gets worse, but I won’t bore you with
> the details since I won’t be doing it. 
> 
> Anyway, while I feel like the implementation for a callback will be extremely straightforward
> and the RFC rather simple, I need to go back and read the original discussion threads for this
> feature first to see if a callback was addressed. So, still not until after 8.4.
> 
> — Rob

It looks like this idea was brought up a couple of times on internals but the conversation always
died out. That being said, it never made it past that point.

...

WeakReference is relatively easy to reason about, I'm thinking something like the following

// WeakReference::onRemove(callable): void
$wr->onRemove(fn() => doSomething());

If your callable closes over the item in the weak reference, I feel that a warning should be
emitted. However, this will depend on implementation details, but it would be a "really nice to
have" since that is a class of errors easily avoided.

As for the lifecycle, this will be called AFTER the item is removed from the weak reference.


WeakMap is a bit more nebulous, but I'm thinking something like the following

// WeakMap::onRemove(callable): void
$wm->onRemove(fn($value) => doSomething($value));

In this case, the callable is given the value of the key (since the key is already gone), but is
otherwise exactly the same as WeakReference. I'm even toying with the idea of omitting the
value altogether.

— Rob


Thread (6 messages)

« previous php.internals (#124396) next »