# Proposal: Adding a RewindableKeyValueIterator type allowing mixed/repeated keys

- Thread: https://externals.io/message/113028
- Original archive: https://news-web.php.net/php.internals/113028
- Started by tyson andre on 2021-01-31T00:36:59+00:00
- Messages: 5
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #113028 — tyson andre — 2021-01-31T00:36:59+00:00

URL: https://externals.io/message/113028#113028

Hi internals,

Currently, there don't seem to be any internal classes that can be used to store a copy of the keys and values of an arbitrary Traversable.

- This would help in eagerly evaluating the result of a generator in a memory efficient way that could be exactly stored and reused 
  e.g. `return $this->cachedResults ??= new \RewindableKeyValueIterator($this->innerResultsGenerator());`
  https://externals.io/message/108767#108797
- This would be useful to exactly represent the keys of sequences with repeated keys (e.g. `yield 'first'; yield 'second';` implicitly uses the key `0` twice.)
- This would be convenient to have to differentiate between 1, '1', and true.
- This would be useful if php were to add internal global functions that act on iterables and return Traversables with potentially repeated keys based on those iterables,
   e.g. map(), filter(), take(), flip(), etc
- If PHP were to add more iterable methods, being able to save an immutable copy of a traversable's result would be useful for end users.
- If PHP were to add a Map (ordered hash map with null/any scalar/arrays/objects as keys) class type in the future,
  and that implemented IteratorAggregate, the return type of getIterator() would need something like RewindableKeyValueIterator.
- The lack of a relevant datatype is among the reasons why classes such as SplObjectStorage are still an Iterator instead of an IteratorAggregate. (and backwards compatibility)

```
final class KeyValueSequenceIterator implements Iterator {
    // loop over all values in $values and store a copy, converting
    // references in top-level array values to non-references
    public function __construct(iterable $values) {...}
    public static function fromKeyValuePairs(iterable $entries): self {...} // fromKeyValuePairs([[$key1, $value1]])
    public function `rewind()`: void {...}
    public function `next()`: void {...}
    public function `current()`: mixed {...}
    public function `key()`: mixed {...}
    public function valid(): bool {...}
    // and __debugInfo, __clone(), etc.
}
```

Thanks,
- Tyson

---

## #113029 — Levi Morrison via internals — 2021-01-31T03:49:23+00:00

URL: https://externals.io/message/113028#113029 · In reply to: #113028

>
> Hi internals,
>
> Currently, there don't seem to be any internal classes that can be used to store a copy of the keys and values of an arbitrary Traversable.
>
> - This would help in eagerly evaluating the result of a generator in a memory efficient way that could be exactly stored and reused
>   e.g. `return $this->cachedResults ??= new \RewindableKeyValueIterator($this->innerResultsGenerator());`
>   https://externals.io/message/108767#108797
> - This would be useful to exactly represent the keys of sequences with repeated keys (e.g. `yield 'first'; yield 'second';` implicitly uses the key `0` twice.)
> - This would be convenient to have to differentiate between 1, '1', and true.
> - This would be useful if php were to add internal global functions that act on iterables and return Traversables with potentially repeated keys based on those iterables,
>    e.g. map(), filter(), take(), flip(), etc
> - If PHP were to add more iterable methods, being able to save an immutable copy of a traversable's result would be useful for end users.
> - If PHP were to add a Map (ordered hash map with null/any scalar/arrays/objects as keys) class type in the future,
>   and that implemented IteratorAggregate, the return type of getIterator() would need something like RewindableKeyValueIterator.
> - The lack of a relevant datatype is among the reasons why classes such as SplObjectStorage are still an Iterator instead of an IteratorAggregate. (and backwards compatibility)
>
> ```
> final class KeyValueSequenceIterator implements Iterator {
>     // loop over all values in $values and store a copy, converting
>     // references in top-level array values to non-references
>     public function __construct(iterable $values) {...}
>     public static function fromKeyValuePairs(iterable $entries): self {...} // fromKeyValuePairs([[$key1, $value1]])
>     public function `rewind()`: void {...}
>     public function `next()`: void {...}
>     public function `current()`: mixed {...}
>     public function `key()`: mixed {...}
>     public function valid(): bool {...}
>     // and __debugInfo, __clone(), etc.
> }
> ```
>
> Thanks,
> - Tyson
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php
>

The names `RewindableKeyValueIterator` and `KeyValueSequenceIterator`
are just long-form descriptions of the Iterator API. I don't think we
need such long names. The name should focus on what it brings.

What it brings is a caching iterator around another iterator, that
includes re-windability. The SPL provides a `CachingIterator`, but I
assume it is inadequate somehow (it is basically SPL tradition). Can
you specifically discuss the shortcomings of `CachingIterator` and how
you will address them? To that end, have you implemented a
proof-of-concept for the proposed iterator?

---

## #113031 — tyson andre — 2021-01-31T15:43:48+00:00

URL: https://externals.io/message/113028#113031 · In reply to: #113029

Hi Levi Morrison,

> > […]
>
> The names `RewindableKeyValueIterator` and `KeyValueSequenceIterator`
> are just long-form descriptions of the Iterator API. I don't think we
> need such long names. The name should focus on what it brings.
>
> What it brings is a caching iterator around another iterator, that
> includes re-windability. The SPL provides a `CachingIterator`, but I
> assume it is inadequate somehow (it is basically SPL tradition). Can
> you specifically discuss the shortcomings of `CachingIterator` and how
> you will address them? To that end, have you implemented a
> proof-of-concept for the proposed iterator?

It caches the results by coercing keys and inserting them into an array, which is a different use case.
If you rewind a CachingIterator, it rewinds the iterator that it wraps,
which throws for Generators and other types of iterators.

For RewindableKeyValueIterator, it needs to store keys that can't be used as array keys.

I was planning to implement a proof of concept if there wasn't widespread opposition
and if nobody pointed out the functionality already existed.

```
<?php
function dump_iterable(iterable $x) {
    foreach ($x as $key => $value) {
        printf("Key: %s\nValue: %s\n", json_encode($key), json_encode($value));
    }
}
function yields_values(): Generator { yield 0 => 'first'; yield '0' => 'second'; }
$c = new CachingIterator(yields_values(), CachingIterator::FULL_CACHE);
echo "First CachingIterator iteration\n";
dump_iterable($c);
var_export($c->getCache()); echo "\n"; // array(0 => 'second') does not represent that data
echo "Second CachingIterator iteration\n";
// Fatal error: Uncaught Exception: Cannot rewind a generator that was already run
dump_iterable($c);
```

Thanks,
- Tyson

---

## #113034 — tyson andre — 2021-01-31T19:13:12+00:00

URL: https://externals.io/message/113028#113034 · In reply to: #113031

Hi Levi Morrison,

> > […]
> 
> It caches the results by coercing keys and inserting them into an array, which is a different use case.
> If you rewind a CachingIterator, it rewinds the iterator that it wraps,
> which throws for Generators and other types of iterators.
> 
> For RewindableKeyValueIterator, it needs to store keys that can't be used as array keys.
> 
> I was planning to implement a proof of concept if there wasn't widespread opposition
> and if nobody pointed out the functionality already existed.
> 
> ```
> <?php
> function dump_iterable(iterable $x) {
>     foreach ($x as $key => $value) {
>         printf("Key: %s\nValue: %s\n", json_encode($key), json_encode($value));
>     }
> }
> function yields_values(): Generator { yield 0 => 'first'; yield '0' => 'second'; }
> $c = new CachingIterator(yields_values(), CachingIterator::FULL_CACHE);
> echo "First CachingIterator iteration\n";
> dump_iterable($c);
> var_export($c->getCache()); echo "\n"; // array(0 => 'second') does not represent that data
> echo "Second CachingIterator iteration\n";
> // Fatal error: Uncaught Exception: Cannot rewind a generator that was already run
> dump_iterable($c);
> ```

A proof of concept is available at https://github.com/php/php-src/pull/6655

The implementation is similar to SplFixedArray, but instead of an array of `n` zvals,
it's an array of `n` pairs of zvals for keys and values.

It's an `Iterator` because IteratorAggregator->getIterator() must eventually return an Iterator,
but I'm consider also proposing an IteratorAggregate such as KeyValueSequence 
that can share the same immutable storage (or copy it).

For example, `someprefix_map(iterable, callable $mapper): KeyValueSequence` could be useful internally to handle
Traversables that could be repeated or use non-scalar keys.

Thanks,
- Tyson

---

## #113051 — tyson andre — 2021-02-02T14:59:25+00:00

URL: https://externals.io/message/113028#113051 · In reply to: #113028

Hi internals,

> Currently, there don't seem to be any internal classes that can be used to store a copy of the keys and values of an arbitrary Traversable.
> 
> - This would help in eagerly evaluating the result of a generator in a memory efficient way that could be exactly stored and reused 
>   e.g. `return $this->cachedResults ??= new \RewindableKeyValueIterator($this->innerResultsGenerator());`
>   https://externals.io/message/108767#108797
> - This would be useful to exactly represent the keys of sequences with repeated keys (e.g. `yield 'first'; yield 'second';` implicitly uses the key `0` twice.)
> - This would be convenient to have to differentiate between 1, '1', and true.
> - This would be useful if php were to add internal global functions that act on iterables and return Traversables with potentially repeated keys based on those iterables,
>    e.g. map(), filter(), take(), flip(), etc
> - If PHP were to add more iterable methods, being able to save an immutable copy of a traversable's result would be useful for end users.
> - If PHP were to add a Map (ordered hash map with null/any scalar/arrays/objects as keys) class type in the future,
>   and that implemented IteratorAggregate, the return type of getIterator() would need something like RewindableKeyValueIterator.
> - The lack of a relevant datatype is among the reasons why classes such as SplObjectStorage are still an Iterator instead of an IteratorAggregate. (and backwards compatibility)
> 
> ```
> final class KeyValueSequenceIterator implements Iterator {
>     // loop over all values in $values and store a copy, converting
>     // references in top-level array values to non-references
>     public function __construct(iterable $values) {...}
>     public static function fromKeyValuePairs(iterable $entries): self {...} // fromKeyValuePairs([[$key1, $value1]])
>     public function `rewind()`: void {...}
>     public function `next()`: void {...}
>     public function `current()`: mixed {...}
>     public function `key()`: mixed {...}
>     public function valid(): bool {...}
>     // and __debugInfo, __clone(), etc.
> }
> ```

This has been updated - the preferred name is CachedIterable.
It turns out InternalIterator is generally more efficient, 
and SplFixedArray is already using that.
The implementation is pretty much done - https://github.com/php/php-src/pull/6655

```
/**
 * Internal class that can be used to eagerly evaluate and store a copy
 * of the keys and values of an arbitrary Traversable.
 * Keys can be any type, and keys can be repeated.
 */
final class CachedIterable implements IteratorAggregate, Countable
{
    public function __construct(iterable $iterator) {}
    public function getIterator(): InternalIterator {}
    public function `count()`: int {}
    public static function fromPairs(array $pairs): CachedIterable;

    public function __serialize(): array {}  // [$k1, $v1, $k2, $v2,...]
    public function __unserialize(array $data): void {}

    // useful for converting iterables back to values
    public function keys(): array {}
    public function values(): array {}
    // useful to efficiently get offsets at the middle/end of a long iterable.
    public function keyAt(int $offset): mixed {}
    public function valueAt(int $offset): mixed {}
    // dynamic properties are forbidden
}
```

Any other comments?

Thanks,
- Tyson
