dakujem / toru
取用 - 轻松处理可迭代集合。Lodash风格的流畅调用链,基于生成器的迭代原语,聚合,实用工具。
Requires
- php: ^8.1
Requires (Dev)
- nette/tester: ^2.4.1
This package is auto-updated.
Last update: 2024-09-26 14:54:28 UTC
README
Toru 是一个独立的可迭代集合工具,适用于简单的日常任务和高级优化。
其大多数功能基于原生的 生成器,以实现大数据集的效率。
💿
composer require dakujem/toru
📒 变更日志
Toru 提供了一些常见的
- 迭代原语(例如
map
、filter
、tap
), - 聚合(例如
reduce
、search
、count
) - 和实用函数(例如
chain
、valuesOnly
、slice
、limit
)
... 使用 生成器 实现。
TL;DR
- 在不转换为数组的情况下转换原生的
iterable
类型集合(迭代器和数组)的元素 - 为每个映射器、过滤器、归约器或效果函数提供键(索引)
- 启用流畅调用链(Lodash风格)
- 延迟评估变换
- 在不增加内存使用的情况下转换大数据集
- 生成器的内存效率比原生数组函数更好
- 比原生数组函数或直接在
foreach
块内部进行转换要慢
*
iterable
是一个内置的编译时类型别名,用于array|Traversable
,包括所有数组和迭代器,因此它不是一个原生类型,从技术上讲。
流畅调用链 允许优雅的变换组合。
_dash($collection) ->filter(predicate: $filterFunction) ->map(values: $mapperFunction) ->chain($moreElements) ->valuesOnly();
Toru 通过利用生成器,在基于每个元素的基础上工作且不分配额外内存,从而实现了对大数据集的内存高效操作。
// no extra memory wasted on creating a filtered array $filtered = Itera::filter(input: $hugeDataSet, predicate: $filterFunction); foreach($filtered as $key => $value){ /* ... */ }
所有可调用参数始终 接收键 和值。
这是与原生函数(如 array_map
、array_reduce
、array_walk
或 array_filter
)相比的一个关键优势。
$addresses = [ 'john.doe@example.com' => 'John Doe', 'jane.doe@example.com' => 'Jane Doe', ]; $recipients = Itera::map( $addresses, fn($recipientName, $recipientAddress) => new Recipient($recipientAddress, $recipientName), ); Mailer::send($mail, $recipients);
🥋
包名来自日语单词 "toru"(取用),可能意味着 "取"、"拾起" 或甚至 "收集"。
示例
任务:以 低内存占用 迭代多个大型数组(或其他可迭代集合)
use Dakujem\Toru\Itera; // No memory wasted on creating a compound array. Especially true when the arrays are huge. $all = Itera::chain($collection1, $collection2, [12,3,5], $otherCollection); foreach ($all as $key => $element) { // do not repeat yourself }
任务:过滤和映射一个集合,也指定新的键(重新索引)
use Dakujem\Toru\Dash; $mailingList = Dash::collect($mostActiveUsers) ->filter( predicate: fn(User $user): bool => $user->hasGivenMailingConsent() ) ->adjust( values: fn(User $user) => $user->fullName(), keys: fn(User $user) => $user->emailAddress(), ) ->limit(100); foreach ($mailingList as $emailAddress => $recipientName) { $mail->addRecipient($emailAddress, $recipientName); }
任务:创建一个包含目录中所有文件的列表,作为 path => FileInfo
对,而不会耗尽内存
$files = _dash( new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)) ) // recursively iterate over a dir ->filter(fn(\SplFileInfo $fileInfo) => !$fileInfo->isDir()) // reject directories ->reindex(fn(\SplFileInfo $fileInfo) => $fileInfo->getPathname()); // index by full file path
请注意,这里我们使用了全局函数 _dash
,您可以在项目中选择性地定义。请参阅下面的“使用全局别名”部分。
用法
以下API部分中描述的大多数原语以 3种形式 实现
- 作为静态方法
Itera::*(iterable $input, ...$args)
,用于简单情况 - 作为
Dash
包装器的流畅方法,Dash::*(...$args): Dash
,最适合流畅组合 - 作为创建部分应用调用的工厂方法,
IteraFn::*(...$args): callable
,可以组合到管道中或用作过滤器(例如在Twig、Blade、Latte中...)
示例:过滤和映射集合,然后附加一些已处理的元素。
使用单个 静态方法
use Dakujem\Toru\Itera; $filtered = Itera::filter(input: $collection, predicate: $filterFunction); $mapped = Itera::apply(input: $filtered, values: $mapperFunction); $merged = Itera::chain($mapped, $moreElements); $processed = Itera::valuesOnly(input: $merged);
使用 Dash
包装器进行流畅 调用链
use Dakujem\Toru\Dash; $processed = Dash::collect($collection) ->filter(predicate: $filterFunction) ->apply(values: $mapperFunction) ->chain($moreElements) ->valuesOnly();
使用 部分应用方法
use Dakujem\Toru\Pipeline; use Dakujem\Toru\IteraFn; $processed = Pipeline::through( $collection, IteraFn::filter(predicate: $filterFunction), IteraFn::apply(values: $mapperFunction), IteraFn::chain($moreElements), IteraFn::valuesOnly(), );
现在可以迭代 $processed
集合。上述所有操作仅在此处、基于每个元素的基础上应用。
foreach ($processed as $value) { // The filtered and mapped values from $collection will appear here, // followed by the elements present in $moreElements. }
API
链式多个可迭代对象:chain
、append
use Dakujem\Toru\Dash; use Dakujem\Toru\Itera; use Dakujem\Toru\IteraFn; Itera::chain(iterable ...$input): iterable // `append` is only present in `Dash` and `IteraFn` classes as an alias to `chain` Dash::append(iterable ...$more): Dash IteraFn::append(iterable ...$more): callable
chain
方法创建一个由所有参数组成的可迭代对象。
结果可迭代对象将依次产生第一个可迭代对象的所有值(保留键),然后是下一个,然后是下一个,依此类推。
与array_replace
(或array_merge
或数组上的联合操作符+
)相比,这非常内存高效,因为它不会加倍内存使用。
append
方法将可迭代对象追加到包装/输入集合中。它是chain
方法的别名。
append
方法仅在IteraFn
和Dash
类中存在。在Itera
类的静态上下文中追加没有意义,因为没有东西可以追加。
在静态上下文中,请使用Itera::chain
。
映射:map
、adjust
、apply
、reindex
、unfold
use Dakujem\Toru\Itera; Itera::adjust(iterable $input, ?callable $values = null, ?callable $keys = null): iterable Itera::apply(iterable $input, callable $values): iterable Itera::map(iterable $input, callable $values): iterable Itera::reindex(iterable $input, callable $keys): iterable Itera::unfold(iterable $input, callable $mapper): iterable
adjust
方法允许映射值和键。apply
方法仅映射值,
而reindex
方法允许映射键(索引)。
不要将map
方法与原生的array_map
混淆,原生函数具有不同的接口。相反,最好使用apply
方法来映射值。map
方法是apply
方法的别名。
对于这些方法中的每一个,所有映射可调用都接收当前键作为第二个参数。
映射器的签名始终是
fn(mixed $value, mixed $key): mixed
unfold
方法允许映射和/或平铺矩阵一级。
使用unfold
和单个可调用映射值和键的一个技巧是返回一个单键值对(包含单个元素的数组,具有指定的键),如下所示
use Dakujem\Toru\Itera; Itera::unfold( [1,2,3,4], fn($value, $key) => ['double value for key ' . $key => $value * 2], ) // or "produce new index and key based on the original value" Itera::unfold( ['one:1', 'two:2', 'three:3'], function(string $value) { [$name, $index] = split(':', $value); // there could be a fairly complex regex here return [$index => $name]; }, )
归约:reduce
这是一个聚合函数,它将立即消耗输入。
与array_reduce
类似,但适用于任何可迭代对象,并将键传递给归约器。
use Dakujem\Toru\Itera; Itera::reduce(iterable $input, callable $reducer, mixed $initial): mixed
归约器的签名是
fn(mixed $carry, mixed $value, mixed $key): iterable|mixed
当使用Dash::reduce
流畅调用时,结果以两种不同的方式处理
- 当返回可迭代值时,结果被包裹在一个新的
Dash
实例中,以允许继续流畅调用链(对矩阵归约很有用) - 当返回其他混合值类型时,结果按原样返回
use Dakujem\Toru\Dash; // The value is returned directly, because it is not iterable: Dash::collect([1,2,3])->reduce(fn() => 42); // 42 // The value `[42]` is iterable, thus a new `Dash` instance is returned: Dash::collect([1,2,3])->reduce(fn() => [42])->count(); // 1
过滤:filter
创建一个生成器,仅产生对输入集合的项,该项的谓词返回真。
use Dakujem\Toru\Itera; Itera::filter(iterable $input, callable $predicate): iterable
接受和消除基于可调用的谓词的元素。
当谓词返回真时,元素被接受并产生。
当谓词返回假时,元素被拒绝并跳过。
谓词的签名是
fn(mixed $value, mixed $key): bool
与array_filter
、iter\filter
类似。
备注
可以用于类似结果的本地
CallbackFilterIterator
new CallbackFilterIterator(Itera::toIterator($input), $predicate)
搜索:search
、searchOrFail
、firstValue
、firstKey
、firstValueOrDefault
、firstKeyOrDefault
这些是聚合函数,它们将立即消耗输入。
use Dakujem\Toru\Itera; Itera::search(iterable $input, callable $predicate, mixed $default = null): mixed Itera::searchOrFail(iterable $input, callable $predicate): mixed Itera::firstValue(iterable $input): mixed Itera::firstKey(iterable $input): mixed Itera::firstValueOrDefault(iterable $input, mixed $default = null): mixed Itera::firstKeyOrDefault(iterable $input, mixed $default = null): mixed
搜索返回谓词返回真的第一个元素。
search
在没有找到匹配元素时返回默认值,而searchOrFail
抛出异常。
firstKey
和firstValue
方法在输入为空集合时抛出异常,而*OrDefault
变体在这种情况返回指定的默认值。
谓词的签名是
fn(mixed $value, mixed $key): bool
切片:slice
、limit
、omit
use Dakujem\Toru\Itera; Itera::limit(iterable $input, int $limit): iterable Itera::omit(iterable $input, int $omit): iterable Itera::slice(iterable $input, int $offset, int $limit): iterable
使用limit
限制产生的元素数量,使用omit
跳过开头的一定数量的元素,或使用slice
将omit
和limit
合并到单个调用中。
键将被保留。
将零或负值传递给$limit
产生一个空集合,
将零或负值传递给$omit
/$offset
产生整个集合。
注意,在省略时,选定的元素数量(
$omit
/$offset
)仍然被迭代,但不会产生。
与array_slice
类似,保留键。
注意
与array_slice
不同,键总是被保留。当需要删除键时,请使用Itera::valuesOnly
。
变更:valuesOnly
、keysOnly
、flip
创建一个只产生值、键或翻转它们的生成器。
use Dakujem\Toru\Itera; Itera::valuesOnly(iterable $input): iterable Itera::keysOnly(iterable $input): iterable Itera::flip(iterable $input): iterable
flip
函数与array_flip
类似,valuesOnly
函数与array_values
类似,
而keysOnly
函数与array_keys
类似。
use Dakujem\Toru\Itera; Itera::valuesOnly(['a' => 'Adam', 'b' => 'Betty']); // ['Adam', 'Betty'] Itera::keysOnly(['a' => 'Adam', 'b' => 'Betty']); // ['a', 'b'] Itera::flip(['a' => 'Adam', 'b' => 'Betty']); // ['Adam' => 'a', 'Betty' => 'b']
转换:toArray
、toArrayValues
、toArrayMerge
、toIterator
、ensureTraversable
这些函数立即使用输入。
将输入从通用iterable
转换为array
/Iterator
。
use Dakujem\Toru\Itera; Itera::toArray(iterable $input): array Itera::toArrayMerge(iterable $input): array Itera::toArrayValues(iterable $input): array Itera::toIterator(iterable $input): \Iterator Itera::ensureTraversable(iterable $input): \Traversable
💡
一般来说,迭代器,特别是生成器,在转换为数组时具有挑战性。请阅读下面的“注意事项”部分。
Itera::toArray(Itera::chain([1,2], [3,4])); // --> [3,4] ❗
“转换为数组”操作有3种变体。
分叉:tap
、each
创建一个生成器,在迭代时对每个元素调用一个效果函数。
use Dakujem\Toru\Itera; Itera::tap(iterable $input, callable $effect): iterable Itera::each(iterable $input, callable $effect): iterable // alias for `tap`
效果函数的签名是
fn(mixed $value, mixed $key): void
返回值被丢弃。
重复:repeat
、loop
、replicate
use Dakujem\Toru\Itera; Itera::repeat(mixed $input): iterable Itera::loop(iterable $input): iterable Itera::replicate(iterable $input, int $times): iterable
repeat
函数无限期地重复输入。loop
函数无限期地产生输入的各个元素。replicate
函数按指定次数产生输入的各个元素。
如果将repeat
和loop
转换为数组,应将其包装在limit
和valuesOnly
中。
请注意,如果loop
和replicate
函数的输入是一个生成器,它们可能会遇到生成器的固有问题——不可重置和具有重叠索引。
生产:make
、produce
produce
函数将创建一个无限生成器,在每次迭代时调用提供的生产函数。
use Dakujem\Toru\Itera; Itera::produce(callable $producer): iterable
它应该与limit
函数一起使用。
use Dakujem\Toru\Itera; Itera::limit(Itera::produce(fn() => rand()), 1000); // a sequence of 1000 pseudorandom numbers Itera::produce(fn() => 42); // an infinite sequence of the answer to life, the universe, and everything Itera::produce(fn(int $i) => $i); // an infinite sequence of integers 0, 1, 2, 3, ...
make
函数从其参数创建可迭代的集合。仅在需要迭代器(生成器)的情况下才使用,否则使用数组。
这两个函数仅作为静态Itera
方法可用。
要同时产生键和值,可以使用unfold
来包装produce
,这将返回键值对。
use Dakujem\Toru\Itera; Itera::unfold( Itera::produce(fn(int $i) => [ calculateKey($i) => calculateValue($i) ]) );
延迟评估
生成器函数是懒加载的。
调用生成器函数会创建一个生成器对象,但不会执行任何代码。
代码在迭代开始时执行(例如,通过foreach
)。
通过将生成器作为输入传递给另一个生成器函数,该生成器被装饰并返回一个新的生成器。这种装饰仍然是懒加载的,并且尚未执行任何代码。
use Dakujem\Toru\Dash; use Dakujem\Toru\Itera; // Create a generator from an input collection. $collection = Itera::apply(input: Itera::filter(input: $input, filter: $filter), values: $mapper); // or using Dash $collection = Dash::collect($input)->filter(filter: $filter)->apply(values: $mapper); // No generator code has been executed so far. // The evaluation of $filter and $mapper callables begins with the first iteration below. foreach($collection as $key => $value) { // Only at this point the mapper and filter functions are executed, // once per element of the input collection. // The generator execution is then _paused_ until the next iteration. }
💡
如果这种迭代在遍历整个集合之前终止(例如,通过break
),则不会为剩余的元素调用调用函数。
这提高了在不知道集合将实际消耗多少元素的情况下,效率。
Toru提供的所有返回iterable
的函数都使用生成器且是懒加载的。
示例:adjust
、map
、chain
、filter
、flip
、tap
、slice
、repeat
其他通常返回mixed
或标量值的函数是聚合,它们会立即迭代并执行生成器代码,耗尽输入中的生成器。
示例:reduce
、count
、search
、toArray
、firstValue
使用键(索引)
所有方法(映射器、断言器、聚合器和效果函数)的可调用参数始终接收键和值。
这是与原生函数(如array_map
、array_reduce
或array_walk
,甚至默认设置下的array_filter
)相比的关键优势。
而不是
$mapper = fn($value, $key) => /* ... */; $predicate = fn($value, $key): bool => /* ... */; $collection = array_filter(array_map($mapper, $array, array_keys($array)), $predicate, ARRAY_FILTER_USE_BOTH);
可能更方便
use Dakujem\Toru\Dash; $mapper = fn($value, $key) => /* ... */; $predicate = fn($value, $key): bool => /* ... */; $collection = Dash::collect($array)->map($mapper)->filter($predicate);
使用array_reduce
这会更复杂,因为没有方法可以将键传递给原生函数。
处理它的一个方法是,将数组值转换为包含索引,并修改reducer以考虑更改后的数据类型。
$myActualReducer = fn($carry, $value, $key) => /**/; // Transform the array into a form that includes keys $transformedArray = array_map(function($key, $value) { return [$value, $key]; }, array_keys($array), $array); // Apply array_reduce $result = array_reduce($transformedArray, function($carry, array $valueAndKey) use ($myActualReducer){ [$value, $key] = $valueAndKey; return $myActualReducer($carry, $value, $key); });
在这里,解决方案可能更简洁
use Dakujem\Toru\Itera; $myActualReducer = fn($carry, $value, $key) => /**/; $result = Itera::reduce($array, $myActualReducer);
自定义转换
为了在使用Dash
时不会中断流畅的调用链,提供了两种方法来支持自定义转换
Dash::alter
- 期望一个返回更改后的可迭代集合的装饰器函数
- 将可迭代结果包装在新的
Dash
实例中,以继续流畅的调用链
Dash::aggregate
- 期望一个返回任何值(
mixed
)的聚合函数 - 结束流畅的链
- 期望一个返回任何值(
Dash::alter
将装饰器的返回值包装到新的Dash
实例中,允许进行流畅的后续调用。
use Dakujem\Toru\Dash; Dash::collect(['zero', 'one', 'two', 'three',]) ->alter(function (iterable $collection): iterable { foreach ($collection as $k => $v) { yield $k * 2 => $v . ' suffix'; } }) ->alter(function (iterable $collection): iterable { foreach ($collection as $k => $v) { yield $k + 1 => 'prefix ' . $v; } }) ->filter( /* ... */ ) ->map( /* ... */ );
Dash::aggregate
返回可调用参数产生的任何值,不将其包装到新的Dash
实例中。
缺少“键求和”函数?需要计算中位数吗?
use Dakujem\Toru\Dash; use Dakujem\Toru\Itera; $keySum = Dash::collect($input) ->filter( /* ... */ ) ->aggregate(function (iterable $collection): int { $keySum = 0; foreach ($collection as $k => $v) { $keySum += $k; } return $keySum; }); // no more fluent calls, integer is returned $median = Dash::collect($input) ->filter( /* ... */ ) ->aggregate(function (iterable $collection): int { return MyCalculus::computeMedian(Itera::toArray($collection)); }); // the median value is returned
扩展Toru
可以通过扩展Dash
类来实现自定义转换或聚合,以便在流畅的调用链中使用。
为了与扩展Dash
保持一致,可以扩展Itera
和IteraFn
类。
use Dakujem\Toru\Dash; use Dakujem\Toru\Itera; class MyItera extends Itera { /** * A shorthand for mapping arrays to use instead of `array_map` * with keys provided for the mapper. */ public static function mapToArray(iterable $input, callable $mapper): iterable { return static::toArray( static::apply(input: $input, values: $mapper), ); } public static function appendBar(iterable $input): iterable { return static::chain($input, ['bar' => 'bar']); } } class MyDash extends Dash { public function appendFoo(): self { return new static( Itera::chain($this->collection, ['foo' => 'foo']) ); } public function appendBar(): self { return new static( MyItera::appendBar($this->collection) ); } public function aggregateZero(): int { return 0; // ¯\_(ツ)_/¯ } } // 0 (zero) MyDash::collect([1, 2, 3])->appendFoo()->appendBar()->aggregateZero(); // [1, 2, 3, 'foo' => 'foo', 'bar' => 'bar'] MyDash::collect([1, 2, 3])->appendFoo()->appendBar()->toArray(); // [1, 2, 3, 'foo' => 'foo', 'bar' => 'bar'] (generator) MyItera::appendBar([1, 2, 3]);
使用全局别名
如果您想要一个全局别名来创建Dash包装的集合,例如_dash
,最佳方式是在您的引导程序中注册全局函数,如下所示
use Dakujem\Toru\Dash; if (!function_exists('_dash')) { function _dash(iterable $input): Dash { return Dash::collect($input); } }
您也可以将此函数定义放置在文件中(例如/bootstrap/dash.php
),并使用Composer自动加载。在您的composer.json
文件中,添加自动加载规则如下
{ "autoload": { "files": [ "bootstrap/dash.php" ] } }
您不再需要导入Dash
类。
_dash($collection)->filter( /* ... */ )->map( /* ... */ )->toArray();
在定义全局函数
_
或__
时要小心,因为它可能会与其他函数(例如Gettext扩展)或常见i8n函数别名发生冲突。
注意事项
生成器虽然功能强大,但也存在一些注意事项
- 处理键(索引)可能很棘手
- 生成器不可重置
在使用Toru之前请理解生成器,这可能有助于避免头痛
📖 生成器概述
📖 生成器语法
将生成器转换为数组时的键注意事项
在将生成器转换为数组时,存在两个原生挑战
- 重叠的键(索引)
- 键类型
重叠的键会在使用iterator_to_array
时导致值被覆盖。
由于生成器可以产生任何类型的键,因此将它们用作数组键可能会导致TypeError
异常。
chain
和toArray
(或iterator_to_array
)的组合类似于原生的array_replace
use Dakujem\Toru\Itera; Itera::toArray( Itera::chain([1, 2], [3, 4]) );
结果将是[3, 4]
,这可能是意外的。原因是可迭代的(在这种情况下是数组)具有重叠的键,在转换为数组时,后面的值会覆盖前面的值。
use Dakujem\Toru\Itera; Itera::toArray( Itera::chain([ 0 => 1, 1 => 2, ], [ 0 => 3, 1 => 4, ]) );
在遍历迭代器时不存在此问题
use Dakujem\Toru\Itera; foreach(Itera::chain([1, 2], [3, 4]) as $key => $value){ echo "$key: $value\n"; }
上面的代码将正确输出
0:1 1:2 0:3 1:4
查看此代码的实际效果: 生成器键冲突
如果我们能够丢弃键,那么最快的解决方案是使用toArrayValues
,它是链式调用Itera::toArray(Itera::valuesOnly( $input ))
的缩写。
如果我们想要模拟array_merge
的行为,Toru提供了toArrayMerge
函数。
这个变体保留了关联键,同时丢弃了数字键。
use Dakujem\Toru\Itera; Itera::toArrayMerge( Itera::chain([ 0 => 1, 1 => 2, 'foo' => 'bar', ], [ 0 => 3, 1 => 4, ]) );
该调用将生成以下数组
[ 0 => 1, 1 => 2, 'foo' => 'bar', 2 => 3, 3 => 4, ]
💡
请注意,生成器通常可以产生任何类型的键,但当转换为数组时,只有可用作原生数组键的值被允许,对于其他值类型的键,将会抛出
TypeError
。
生成器不可重置
一旦生成器的迭代开始,对其调用 rewind
将会抛出错误。可以使用提供的 Regenerator
迭代器解决这个问题(见下文)。
支持内容
Regenerator
Regenerator
是对返回迭代器的可调用对象的透明包装,特别是 生成器对象。这些可以是直接的 生成器函数,或者包装它们的可调用对象。
Regenerator
允许 重置生成器,这在一般情况下是不允许的。生成器实际上并没有被重置,但在每次重置时都会重新创建。
由于本库中的大多数迭代原语都是使用生成器实现的,这可能会很有用。
注意:使用
foreach
迭代时,重置会自动发生。
让我们用一个例子来说明。
$generatorFunction = function(): iterable { yield 1; yield 2; yield 'foo' => 'bar'; yield 42; }; $generatorObject = $generatorFunction(); foreach ($generatorObject as $key => $val) { /* ... */ } // subsequent iteration will throw an exception // Exception: Cannot rewind a generator that was already run foreach ($generatorObject as $key => $val) { /* ... */ }
可以通过对每个迭代重复调用 生成器函数 来解决这个问题。
// Instead of iterating over the same generator object, the generator function // is called multiple times. Each call creates a new generator object. foreach ($generatorFunction() as $key => $val) { /* ... */ } // A new generator object is created with every call to $generatorFunction(). foreach ($generatorFunction() as $key => $val) { /* ... */ } // ✅ no exception
在大多数情况下,这将是解决方案,但有时需要 iterable
/Traversable
对象。
use Dakujem\Toru\Dash; // Not possible, the generator function is not iterable itself $it = Dash::collect($generatorFunction); // TypeError // Not possible, the argument to `collect` must be iterable $it = Dash::collect(fn() => $generatorFunction()); // TypeError // The correct way is to wrap the generator returned by the call, // but it has the same drawback as described above $dash = Dash::collect($generatorFunction()); foreach ($dash->filter($filterFn) as $val) { /* ... */ } // Exception: Cannot rewind a generator that was already run foreach ($dash->filter($otherFilterFn) as $val) { /* ... */ } // fails
这就是 Regenerator
发挥作用的地方。
use Dakujem\Toru\Dash; use Dakujem\Toru\Regenerator; $dash = new Regenerator(fn() => Dash::collect($generatorFunction())); foreach ($dash->filter($filterFn) as $val) { /* ... */ } foreach ($dash->filter($otherFilterFn) as $val) { /* ... */ } // works, hooray!
Regenerator
在需要时(即每次重置时)内部调用提供函数,同时实现 Traversable
接口。
存储中间值
由于大多数 Toru 函数调用都返回生成器,将中间值存储在变量中会遇到同样的问题。
use Dakujem\Toru\Itera; $filtered = Itera::filter($input, $predicate); $mapped = Itera::apply($filtered, $mapper); foreach($mapped as $k => $v) { /* ...*/ } // Exception: Cannot rewind a generator that was already run foreach($filtered as $k => $v) { /* ...*/ }
同样,解决方案可能是创建一个函数,如下所示
use Dakujem\Toru\Itera; $filtered = fn() => Itera::filter($input, $predicate); $mapped = fn() => Itera::apply($filtered(), $mapper); foreach($mapped() as $k => $v) { /* ...*/ } // this will work, the issue is mitigated by iterating over a new generator foreach($filtered() as $k => $v) { /* ...*/ }
或者,Regenerator
类也很有用。
use Dakujem\Toru\Itera; use Dakujem\Toru\Regenerator; $filtered = new Regenerator(fn() => Itera::filter($input, $predicate)); $mapped = new Regenerator(fn() => Itera::apply($filtered(), $mapper)); foreach($mapped as $k => $v) { /* ...*/ } // In this case, the regenerator handles function calls. foreach($filtered as $k => $v) { /* ...*/ }
管道
这是一个简单的处理管道实现。与 IteraFn
类结合使用,可以组合处理算法。
use Dakujem\Toru\IteraFn; use Dakujem\Toru\Pipeline; $alteredCollection = Pipeline::through( $collection, IteraFn::filter(predicate: $filterFunction), IteraFn::map(values: $mapper), ); // Pipelines are not limited to producing iterable collections, they may produce any value types: $average = Pipeline::through( $collection, IteraFn::filter(predicate: $filterFunction), IteraFn::reduce(reducer: fn($carry, $current) => $carry + $current, initial: 0), fn(int $sum) => $sum / $sizeOfCollection, );
为什么需要迭代器
PHP 已经提供了对数组的广泛支持,为什么还需要迭代器呢?
有很多情况下,迭代器可能比数组更高效。通常当处理大(或可能甚至无限)集合时。
迭代器的使用场景类似于 流 资源。你知道将上传的文件放入字符串变量并不是总是最好的主意。对于小文件来说,这肯定没问题,但试着用 4K 视频试试。
一个很好的例子可能是目录迭代器。
可能会有多少个文件?几十个?还是几百万个?将这些放入数组可能会很快耗尽应用程序的内存储备。
那么为什么使用 iterable
类型提示而不是 array
呢?
仅仅是为了尽可能扩展函数/方法的潜在用途。
内存效率
生成器的效率来源于这样一个事实:在进行链式多个集合、过滤、映射等操作时,不需要分配额外的内存。
另一方面,foreach
块总是执行得更快,因为没有涉及额外的函数调用。根据您的用例,性能差异可能可以忽略不计。
然而,在云环境中,内存可能很昂贵。这是一个权衡。
在实际场景中,如果启用了 OpCache,使用 Toru 将会以最小的/可忽略不计的影响减少内存使用。
例如,将多个集合链接到一起而不是使用
array_merge
将会更高效。https://3v4l.org/Ymksm
https://3v4l.org/OmUb3
https://3v4l.org/HMasj如果您对性能感到担忧,请使用
/tests/performance
中的比较脚本与您的实际环境进行比较。
替代方案
您可能不需要这个库。
mpetrovich/dash
提供了一系列转换函数,内部使用 数组lodash-php/lodash-php
模仿 Lodash,并提供了一系列实用工具,内部使用 数组nikic/iter
使用 生成器 实现了一系列迭代原语,由 PHP 核心团队成员编写illuminate/collections
应该能满足大多数 Laravel 开发者的需求,提供基于数组和基于生成器的两种实现- 在许多情况下,使用
foreach
就能完成任务
Toru 库(dakujem/toru
)不提供完整的预定义转换函数,而是提供最常见的函数以及引入和组合自定义转换的方法。
它与上述库以及其他类似的库配合得很好。
Toru 最初是作为 nikic/iter
的替代品开始使用的,后者对我来说界面有点繁琐。Itera
静态 类 尝试通过使用单个类导入而不是多个函数导入,并重新排序参数,使输入集合始终是第一个,来解决这一点。
然而,将多个操作组合成一个转换感觉有点繁琐,因此实现了 IteraFn
工厂来解决这个问题。它工作得很好,但对于日常任务来说,仍然有点冗长。
为了允许简洁的流畅/链式调用(如 Lodash),然后设计了 Dash
类。
有了它,就可以整洁地组合转换。
贡献和未来开发
意图不是提供大量特定的函数,而是提供大多数使用情况下的工具。
尽管如此,高质量的 PR 仍将被接受。
可能的添加包括
combine
值和键zip
多个可迭代对象(Python,Haskell 等)alternate
多个可迭代对象(负载均衡元素,混合)
附录:带注释的示例代码
各种方法的说明
观察下面的代码,了解 foreach
和 Dash
如何解决一个简单问题。了解何时以及为什么 Dash
可能比单独使用 Itera
更合适。
use Dakujem\Toru\Itera; use Dakujem\Toru\IteraFn; use Dakujem\Toru\Pipeline; use Dakujem\Toru\Dash; $sequence = Itera::produce(fn() => rand()); // infinite iterator // A naive foreach may be the best solution in certain cases... $count = 0; $array = []; foreach ($sequence as $i) { if (0 == $i % 2) { $array[$i] = 'the value is ' . $i; if ($count >= 1000) { break; } } } // While the standalone static methods may be handy, // they are not suited for complex computations. $interim = Itera::filter($sequence, fn($i) => 0 == $i % 2); $interim = Itera::reindex($interim, fn($i) => $i); $interim = Itera::apply($interim, fn($i) => 'the value is ' . $i); $interim = Itera::limit($interim, 1000); $array = Itera::toArray($interim); // Without the interim variable(s), the reading order of the calls is reversed // and the whole computation is not exactly legible. $array = Itera::toArray( Itera::limit( Itera::apply( Itera::reindex( Itera::filter( $sequence, fn($i) => 0 == $i % 2, ), fn($i) => $i, ), fn($i) => 'the value is ' . $i, ), 1000, ) ); // Complex pipelines may be composed using partially applied callables. $array = Pipeline::through( $sequence, IteraFn::filter(fn($i) => 0 == $i % 2), IteraFn::reindex(fn($i) => $i), IteraFn::apply(fn($i) => 'the value is ' . $i), IteraFn::limit(1000), IteraFn::toArray(), ); // Lodash-style fluent call chaining. $array = Dash::collect($sequence) ->filter(fn($i) => 0 == $i % 2) ->reindex(fn($i) => $i) ->map(fn($i) => 'the value is ' . $i) ->limit(1000) ->toArray();
示例:列出目录中的图像
让我们解决一个简单的任务:递归列出目录中的所有图像。
您也可以让生成式 AI 做这件事,或者提出类似这样的方案
$images = []; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); /** @var SplFileInfo $fileInfo */ foreach ($iterator as $fileInfo) { // Skip directories if ($fileInfo->isDir()) { continue; } // Get the full path of the file $filePath = $fileInfo->getPathname(); // Reject non-image files (hacky) if (!@getimagesize($filePath)) { continue; } $images[$filePath] = $fileInfo; }
这将在开发中工作,但如果尝试列出 数百万 张图像,将对您的服务器产生巨大影响,这在中型内容项目中并不罕见。
解决这个问题的方式是利用生成器
$listImages = function(string $dir): Generator { $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); /** @var SplFileInfo $fileInfo */ foreach ($iterator as $fileInfo) { // Skip directories if ($fileInfo->isDir()) { continue; } // Get the full path of the file $filePath = $fileInfo->getPathname(); // Reject non-image files (hacky) if (!@getimagesize($fileInfo->getPathname())) { continue; } yield $filePath => $fileInfo; } }; $images = $listImages($dir);
那么,如果您能创建一个等效的生成器,就像这样...
$images = _dash(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir))) // recursively iterate over a dir ->filter(fn(SplFileInfo $fileInfo) => !$fileInfo->isDir()) // reject directories ->filter(fn(SplFileInfo $fileInfo) => @getimagesize($fileInfo->getPathname())) // accept only images (hacky) ->reindex(fn(SplFileInfo $fileInfo) => $fileInfo->getPathname()); // key by the full file path
现在这取决于个人喜好。两种方法都能解决问题,并且效率相同。