Hello internals,
I know now that some of you are swamped getting ready for 8.4, so I want to be clear that this will be an 8.5+ thing. I am simply sending it to the list in case anyone wants to discuss it, burn it with fire, give it a gold star, or ignore until later.
Currently, a WeakMap may only have object keys, and this logically makes a lot of sense. However, there are cases where you want to reference an object by a value but if the value is no longer referenced, no longer need it. A good example of this might be used in dependency injection containers, lookup tables for value objects, etc.
I'd like to propose a ReverseWeakMap where the keys must be scalar, and given an object as the value. Once the value is no longer referenced, the key and value is removed from the ReverseWeakMap.
— Rob
Hello internals,
I know now that some of you are swamped getting ready for 8.4, so I want to be clear that this will be an 8.5+ thing. I am simply sending it to the list in case anyone wants to discuss it, burn it with fire, give it a gold star, or ignore until later.
Currently, a WeakMap may only have object keys, and this logically makes a lot of sense. However, there are cases where you want to reference an object by a value but if the value is no longer referenced, no longer need it. A good example of this might be used in dependency injection containers, lookup tables for value objects, etc.
I'd like to propose a ReverseWeakMap where the keys must be scalar, and given an object as the value. Once the value is no longer referenced, the key and value is removed from the ReverseWeakMap.
Isn't this use case already solved with WeakReferences?
https://www.php.net/manual/en/class.weakreference.php
Best regards,
Gina P. Banyard
Hello internals,
I know now that some of you are swamped getting ready for 8.4, so I want to be clear that this will be an 8.5+ thing. I am simply sending it to the list in case anyone wants to discuss it, burn it with fire, give it a gold star, or ignore until later.
Currently, a WeakMap may only have object keys, and this logically makes a lot of sense. However, there are cases where you want to reference an object by a value but if the value is no longer referenced, no longer need it. A good example of this might be used in dependency injection containers, lookup tables for value objects, etc.
I'd like to propose a ReverseWeakMap where the keys must be scalar, and given an object as the value. Once the value is no longer referenced, the key and value is removed from the ReverseWeakMap.
Isn't this use case already solved with WeakReferences?
https://www.php.net/manual/en/class.weakreference.phpBest regards,
Gina P. Banyard
Hey Gina,
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.
Now that I think about it, it might be simpler to add an “onRemove()” method that takes a callback for the WeakReference class.
— Rob
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 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
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