add native fast paths for eager transforms
Arrays and php-ds collections are transformed directly instead of through the lazy generators. merge() into an array now rejects keys that can't be array keys with a clear error, like toArray().
This commit is contained in:
parent
4fe9da0167
commit
f3f05fd00c
@ -14,10 +14,13 @@ parameters:
|
|||||||
message: '#^Function Kadet\\Functional\\Collections\\\w+\(\) should return Closure<TIn of iterable>\(TIn\): \(TIn is array \?#'
|
message: '#^Function Kadet\\Functional\\Collections\\\w+\(\) should return Closure<TIn of iterable>\(TIn\): \(TIn is array \?#'
|
||||||
path: src/Collections/transforms.php
|
path: src/Collections/transforms.php
|
||||||
-
|
-
|
||||||
# toArray() keeps PHP's own coercion of bool, float and null keys, as an array literal would.
|
# toArray() and merge() into an array keep PHP's own coercion of bool, float and null keys, as an array
|
||||||
|
# literal would.
|
||||||
identifier: offsetAccess.invalidOffset
|
identifier: offsetAccess.invalidOffset
|
||||||
message: '#^Possibly invalid array key type bool\|float\|int\|string\|null\.$#'
|
message: '#^Possibly invalid array key type \(?bool#'
|
||||||
path: src/Collections/collectors.php
|
paths:
|
||||||
|
- src/Collections/collectors.php
|
||||||
|
- src/Collections/transforms.php
|
||||||
|
|
||||||
includes:
|
includes:
|
||||||
# Errors in the code that predates PHPStan in this project (untyped pre-1.0 functions and classes). New code
|
# Errors in the code that predates PHPStan in this project (untyped pre-1.0 functions and classes). New code
|
||||||
|
|||||||
@ -70,10 +70,7 @@ function keyedLike(iterable $input, iterable $entries): iterable
|
|||||||
*/
|
*/
|
||||||
function listLike(iterable $input, iterable $values): iterable
|
function listLike(iterable $input, iterable $values): iterable
|
||||||
{
|
{
|
||||||
$list = [];
|
$list = is_array($values) ? array_values($values) : iterator_to_array($values, false);
|
||||||
foreach ($values as $value) {
|
|
||||||
$list[] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($input)) {
|
if (is_array($input)) {
|
||||||
return $list;
|
return $list;
|
||||||
@ -86,6 +83,35 @@ function listLike(iterable $input, iterable $values): iterable
|
|||||||
return seqOf($list);
|
return seqOf($list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What an eager transform iterates: the values of `Ds\Seq` and `Ds\Set` as an array (keyed by position, as
|
||||||
|
* iterating them gives), as iterating an array is faster; anything else as it is.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param iterable<mixed, mixed> $input
|
||||||
|
*
|
||||||
|
* @return iterable<mixed, mixed>
|
||||||
|
*/
|
||||||
|
function iterated(iterable $input): iterable
|
||||||
|
{
|
||||||
|
return $input instanceof \Ds\Seq || $input instanceof \Ds\Set ? $input->toArray() : $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The values of an iterable as a list.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param iterable<mixed, mixed> $iterable
|
||||||
|
*
|
||||||
|
* @return list<mixed>
|
||||||
|
*/
|
||||||
|
function valuesOf(iterable $iterable): array
|
||||||
|
{
|
||||||
|
return is_array($iterable) ? array_values($iterable) : iterator_to_array(iterated($iterable), false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a list in a `Ds\Seq`, or returns it as it is when php-ds is unavailable.
|
* Wraps a list in a `Ds\Seq`, or returns it as it is when php-ds is unavailable.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -23,10 +23,13 @@ namespace Kadet\Functional\Collections;
|
|||||||
use Kadet\Functional\Collections\Lazy as lazy;
|
use Kadet\Functional\Collections\Lazy as lazy;
|
||||||
use function Kadet\Functional\Internal\callback;
|
use function Kadet\Functional\Internal\callback;
|
||||||
use function Kadet\Functional\Collections\Internal\entries;
|
use function Kadet\Functional\Collections\Internal\entries;
|
||||||
|
use function Kadet\Functional\Collections\Internal\iterated;
|
||||||
use function Kadet\Functional\Collections\Internal\keyedLike;
|
use function Kadet\Functional\Collections\Internal\keyedLike;
|
||||||
|
use function Kadet\Functional\Collections\Internal\seen;
|
||||||
use function Kadet\Functional\Collections\Internal\listLike;
|
use function Kadet\Functional\Collections\Internal\listLike;
|
||||||
use function Kadet\Functional\Collections\Internal\seqOf;
|
use function Kadet\Functional\Collections\Internal\seqOf;
|
||||||
use function Kadet\Functional\Collections\Internal\unpairs;
|
use function Kadet\Functional\Collections\Internal\unpairs;
|
||||||
|
use function Kadet\Functional\Collections\Internal\valuesOf;
|
||||||
use function Kadet\Functional\compile;
|
use function Kadet\Functional\compile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,9 +53,29 @@ use function Kadet\Functional\compile;
|
|||||||
*/
|
*/
|
||||||
function map(callable $fn): \Closure
|
function map(callable $fn): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\map($fn);
|
$fn = callback($fn);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
if (is_array($input)) {
|
||||||
|
$result = [];
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
$result[$key] = $fn($value, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input instanceof \Ds\Map) {
|
||||||
|
return $input->map(static fn ($key, $value) => $fn($value, $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach (iterated($input) as $key => $value) {
|
||||||
|
$result[] = $fn($value, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $result);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,9 +98,29 @@ function map(callable $fn): \Closure
|
|||||||
*/
|
*/
|
||||||
function filter(?callable $fn = null): \Closure
|
function filter(?callable $fn = null): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\filter($fn);
|
$fn = $fn === null ? static fn ($value) => $value : callback($fn);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
if ($input instanceof \Ds\Map) {
|
||||||
|
return $input->filter(static fn ($key, $value) => $fn($value, $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = iterated($input);
|
||||||
|
if (is_array($values)) {
|
||||||
|
$values = array_filter($values, $fn, ARRAY_FILTER_USE_BOTH);
|
||||||
|
|
||||||
|
return is_array($input) ? $values : listLike($input, $values);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
if ($fn($value, $key)) {
|
||||||
|
$result[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $result);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,9 +141,32 @@ function filter(?callable $fn = null): \Closure
|
|||||||
*/
|
*/
|
||||||
function reject(callable $fn): \Closure
|
function reject(callable $fn): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\reject($fn);
|
$fn = callback($fn);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
if ($input instanceof \Ds\Map) {
|
||||||
|
return $input->filter(static fn ($key, $value) => !$fn($value, $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
if (is_array($input)) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
if (!$fn($value, $key)) {
|
||||||
|
$result[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (iterated($input) as $key => $value) {
|
||||||
|
if (!$fn($value, $key)) {
|
||||||
|
$result[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $result);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,9 +188,27 @@ function reject(callable $fn): \Closure
|
|||||||
*/
|
*/
|
||||||
function flatMap(callable $fn): \Closure
|
function flatMap(callable $fn): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\flatMap($fn);
|
$fn = callback($fn);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
$result = [];
|
||||||
|
foreach (iterated($input) as $key => $value) {
|
||||||
|
$items = $fn($value, $key);
|
||||||
|
|
||||||
|
if (!is_iterable($items)) {
|
||||||
|
throw new \UnexpectedValueException(sprintf(
|
||||||
|
'flatMap(): the callback must return an iterable, got %s.',
|
||||||
|
get_debug_type($items),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$result[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $result);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,9 +218,7 @@ function flatMap(callable $fn): \Closure
|
|||||||
*/
|
*/
|
||||||
function values(): \Closure
|
function values(): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\values();
|
return static fn (iterable $input): iterable => listLike($input, valuesOf($input));
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -146,12 +228,17 @@ function values(): \Closure
|
|||||||
*/
|
*/
|
||||||
function keys(): \Closure
|
function keys(): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\keys();
|
return static function (iterable $input): iterable {
|
||||||
|
if (is_array($input)) {
|
||||||
|
return array_keys($input);
|
||||||
|
}
|
||||||
|
|
||||||
return static function (iterable $input) use ($lazy): iterable {
|
$keys = [];
|
||||||
$keys = iterator_to_array($lazy($input), false);
|
foreach (iterated($input) as $key => $_) {
|
||||||
|
$keys[] = $key;
|
||||||
|
}
|
||||||
|
|
||||||
return is_array($input) ? $keys : seqOf($keys);
|
return seqOf($keys);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,9 +271,34 @@ function unique(): \Closure
|
|||||||
*/
|
*/
|
||||||
function uniqueBy(callable $selector): \Closure
|
function uniqueBy(callable $selector): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\uniqueBy($selector);
|
$selector = callback($selector);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return static function (iterable $input) use ($selector): iterable {
|
||||||
|
$seen = seen();
|
||||||
|
|
||||||
|
if ($input instanceof \Ds\Map) {
|
||||||
|
return $input->filter(static fn ($key, $value) => !$seen($selector($value, $key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
if (is_array($input)) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
if (!$seen($selector($value, $key))) {
|
||||||
|
$result[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (iterated($input) as $key => $value) {
|
||||||
|
if (!$seen($selector($value, $key))) {
|
||||||
|
$result[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $result);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,7 +373,17 @@ function slice(int $offset, ?int $length = null): \Closure
|
|||||||
{
|
{
|
||||||
$lazy = lazy\slice($offset, $length);
|
$lazy = lazy\slice($offset, $length);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return static function (iterable $input) use ($offset, $length, $lazy): iterable {
|
||||||
|
if (is_array($input)) {
|
||||||
|
return array_slice($input, $offset, $length, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input instanceof \Ds\Seq || $input instanceof \Ds\Map || $input instanceof \Ds\Set) {
|
||||||
|
return $length === null ? $input->slice($offset) : $input->slice($offset, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyedLike($input, $lazy($input));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -271,9 +393,11 @@ function slice(int $offset, ?int $length = null): \Closure
|
|||||||
*/
|
*/
|
||||||
function take(int $n): \Closure
|
function take(int $n): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\take($n);
|
if ($n < 0) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('take(): count must be >= 0, got %d', $n));
|
||||||
|
}
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return slice(0, $n);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -283,9 +407,11 @@ function take(int $n): \Closure
|
|||||||
*/
|
*/
|
||||||
function skip(int $n): \Closure
|
function skip(int $n): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\skip($n);
|
if ($n < 0) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('skip(): count must be >= 0, got %d', $n));
|
||||||
|
}
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
return slice($n);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,9 +425,9 @@ function skip(int $n): \Closure
|
|||||||
*/
|
*/
|
||||||
function prepend(mixed ...$values): \Closure
|
function prepend(mixed ...$values): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\prepend(...$values);
|
$values = array_values($values);
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
return static fn (iterable $input): iterable => listLike($input, array_merge($values, valuesOf($input)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -329,9 +455,7 @@ function append(mixed ...$values): \Closure
|
|||||||
*/
|
*/
|
||||||
function concat(iterable $other): \Closure
|
function concat(iterable $other): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\concat($other);
|
return static fn (iterable $input): iterable => listLike($input, array_merge(valuesOf($input), valuesOf($other)));
|
||||||
|
|
||||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,10 +472,31 @@ function concat(iterable $other): \Closure
|
|||||||
*/
|
*/
|
||||||
function merge(iterable $other): \Closure
|
function merge(iterable $other): \Closure
|
||||||
{
|
{
|
||||||
return static fn (iterable $input): iterable => keyedLike($input, (static function () use ($input, $other) {
|
return static function (iterable $input) use ($other): iterable {
|
||||||
yield from $input;
|
if (is_array($input)) {
|
||||||
yield from $other;
|
foreach ($other as $key => $value) {
|
||||||
})());
|
if (!is_scalar($key) && $key !== null) {
|
||||||
|
throw new \InvalidArgumentException(sprintf(
|
||||||
|
'merge(): key of type %s cannot be an array key',
|
||||||
|
get_debug_type($key),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$input[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input instanceof \Ds\Map) {
|
||||||
|
$result = $input->copy();
|
||||||
|
$result->putAll($other);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, array_merge(valuesOf($input), valuesOf($other)));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -375,18 +520,24 @@ function merge(iterable $other): \Closure
|
|||||||
*/
|
*/
|
||||||
function each(callable $fn): \Closure
|
function each(callable $fn): \Closure
|
||||||
{
|
{
|
||||||
$lazy = lazy\each($fn);
|
$fn = callback($fn);
|
||||||
|
|
||||||
return static function (iterable $input) use ($lazy): iterable {
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
$reusable = is_array($input) || $input instanceof \Ds\Seq || $input instanceof \Ds\Map || $input instanceof \Ds\Set;
|
if (is_array($input) || $input instanceof \Ds\Seq || $input instanceof \Ds\Map || $input instanceof \Ds\Set) {
|
||||||
if ($reusable) {
|
foreach (iterated($input) as $key => $value) {
|
||||||
foreach ($lazy($input) as $_) {
|
$fn($value, $key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $input;
|
return $input;
|
||||||
}
|
}
|
||||||
|
|
||||||
return listLike($input, $lazy($input));
|
$values = [];
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
$fn($value, $key);
|
||||||
|
$values[] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $values);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -364,6 +364,17 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSeq([1, 2, 3], c\merge([3])(self::gen([1, 2])));
|
$this->assertSeq([1, 2, 3], c\merge([3])(self::gen([1, 2])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMergeObjectKeyIntoArrayThrows()
|
||||||
|
{
|
||||||
|
$map = new Map();
|
||||||
|
$map->put(new \stdClass(), 1);
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('merge(): key of type stdClass cannot be an array key');
|
||||||
|
|
||||||
|
c\merge($map)(['a' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
// each / tap
|
// each / tap
|
||||||
|
|
||||||
public function testEachDoesNotReplaceValues()
|
public function testEachDoesNotReplaceValues()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user