bonami/collections

以不可变性和函数式方法为重点的集合库

0.5.2 2023-03-21 16:33 UTC

README

Build Status Latest Stable Version License

目录

动机

为什么还需要为PHP创建另一个集合库?原生的PHP数组或SPL结构(如SplFixedArray或SplObjectStorage等)是可变的,并且具有非常奇怪的用户界面和行为。它们通常一次代表多个数据结构(例如,SplObjectStorage代表集合和映射),它们的接口是为经典的过程式方法设计的。

我们尝试设计结构的接口,使其专注于声明式方法,利用函数式编程。为了更高的安全性,我们设计了结构使其不可变(我们也有一些可变结构,因为有时出于性能原因这是必要的)

所有代码都设计为使用 phpstan 泛型 来确保类型安全。

展示代码!

代码示例胜过千言万语,所以这里有一些简单的示例

过滤Person DTO并提取一些信息

use Bonami\Collection\ArrayList;

class Person {

	public function __construct(
		private readonly string $name,
		private readonly int $age
	) {}

}

$persons = ArrayList::of(new Person('John', 31), new Person('Jacob', 22), new Person('Arthur', 29));
$names = $persons
	->filter(fn (Person $person): bool => $person->age <= 30)
	->sort(fn (Person $a, Person $b): int => $a->name <=> $b->name)
	->map(fn (Person $person): string => $person->name)
	->join(";");

// $names = "Arthur;Jacob"

生成组合

use Bonami\Collection\ArrayList;

$colors = ArrayList::fromIterable(['red', 'green', 'blue']);
$objects = ArrayList::fromIterable(['car', 'pencil']);

$coloredObjects = ArrayList::fromIterable($colors)
	->flatMap(fn (string $color) => $objects->map(fn (string $object) => "{$color} {$object}"))

// $coloredObjects = ArrayList::of('red car', 'red pencil', 'green car', 'green pencil', 'blue car', 'blue pencil')

使用 提升操作符 生成组合

use Bonami\Collection\ArrayList;

$concat = fn (string $first, string $second) => "{$first} {$second}";
$coloredObjects = ArrayList::lift2($concat)($colors, $objects);

字符频率分析

use Bonami\Collection\ArrayList;
use Bonami\Collection\Map;
use function Bonami\Collection\identity;
use function Bonami\Collection\descendingComparator;

function frequencyAnalysis(string $text): Map {
	$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
	return ArrayList::fromIterable($chars)
		->groupBy(identity())
		->mapValues(fn (ArrayList $group): int => $group->count());
}

$text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras nec mi rhoncus, dignissim tortor ac,' .
    ' aliquam metus. Maecenas non hendrerit tellus. Nam molestie augue ac lectus cursus consequat. Nunc ' .
    'ultrices metus sit amet nulla blandit lacinia. Nam vestibulum ultrices mollis. Morbi consequat ante non ' .
    'ornare lobortis. Nullam enim mauris, tempus quis auctor eu, condimentum dignissim nunc. Integer dapibus ' .
    'dolor eu nisl euismod sagittis. Phasellus magna ante, pharetra eget nisi vehicula, elementum lacinia dui. ' .
    'Aliquam semper at eros a sodales. In a rhoncus sapien. Integer blandit volutpat nisl. Donec vitae massa eget ' .
    'mauris dignissim cursus nec et erat. Suspendisse consectetur ac quam sit amet pretium.';

// top ten characters by number of occurrences
$top10 = frequencyAnalysis($text)
    ->sortValues(descendingComparator())
    ->take(10);

特性

结构

  • \Bonami\Collection\ArrayList - 一个不可变(非关联)数组包装器,旨在进行顺序处理。
  • \Bonami\Collection\Map - 一个不可变的键值结构。它可以包含任何类型的对象作为键(有一些限制,请参阅文档中的更多信息)。
  • \Bonami\Collection\Mutable\Map - Map的可变版本。
  • \Bonami\Collection\LazyList - 任何可迭代结构的包装器。它通过内部使用yield来利用惰性。它可以显著节省内存。
  • \Bonami\Collection\Enum - 不是一个集合,但与库的其余部分具有很好的协同作用。用于定义封闭枚举。提供获取给定枚举值的补集列表等有趣的方法。
  • \Bonami\Collection\EnumList - 枚举列表,扩展ArrayList
  • \Bonami\Collection\Option - 用于表示可能具有值和可能不具有值的不可变结构。它提供了一种安全(函数式)的方法来处理空指针错误。
  • \Bonami\Collection\TrySafe - 用于表示在过程中生成值或错误的不可变结构。它提供了一种安全(函数式)的方法来处理错误,而不产生副作用。
  • \Bonami\Collection\CurriedFunction - 表示单参数函数。它可以创建多参数函数的柯里化版本,这对于某些函数式编程组合模式更有利。

类型安全

我们使用 phpstan 注解来提高类型安全性,并利用泛型。为了更好的类型解析,我们创建了一个可选依赖项 phpstan-collections,我们强烈建议您如果使用phpstan,则安装它。它修复了一些类型解析,特别是对于后期静态绑定。

遍历

您可能会遇到这样的情况:使用映射函数将列表映射到返回包装在 Option 中的值,但您更希望得到未包装的值。这时,traverse 方法就派上用场了。

use Bonami\Collection\ArrayList;
use Bonami\Collection\Option;

$getUserNameById = function(int $id): Option {
	$userNamesById = [
		1 => "John",
		2 => "Paul",
		3 => "George",
		4 => "Ringo",
	];
	return Option::fromNullable($userNamesById[$id] ?? null);
};

print Option::traverse(ArrayList::fromIterable([1, 3, 4]), $getUserNameById); 
// Some([John, Paul, Ringo])

将结果与我们的老朋友 ArrayList::map 的用法进行比较。

use Bonami\Collection\ArrayList;
use Bonami\Collection\Option;

$getUserNameById = function(int $id): Option {
	$userNamesById = [
		1 => "John",
		2 => "Paul",
		3 => "George",
		4 => "Ringo",
	];
	return Option::fromNullable($userNamesById[$id] ?? null);
};

print ArrayList::fromIterable([1, 3, 4])
    ->map($getUserNameById);
// [Some(John), Some(George), Some(Ringo)]

您注意到区别了吗?在这里我们有包含字符串的选项列表,而在第一个代码示例中,我们有包含字符串列表的选项。

所以 traverse 允许我们将 Options 列表转换为包含未包装值的 Option 列表。而且,您猜怎么着——像往常一样,None 会毁掉一切。

use Bonami\Collection\ArrayList;
use Bonami\Collection\Option;

$getUserNameById = function(int $id): Option {
	$userNamesById = [
		1 => "John",
		2 => "Paul",
		3 => "George",
		4 => "Ringo",
	];
	return Option::fromNullable($userNamesById[$id] ?? null);
};

print Option::traverse(ArrayList::fromIterable([1, 3, 666]), $getUserNameById); 
// None

traverse 方法的使用不仅限于 Option 类。它适用于任何可应用的对象,所以它也适用于 TrySafeArrayListLazyListFailure 和空列表实例的行为与 None 相同)。

许可证

本软件包在 MIT 许可证 下发布。

贡献

如果您希望为项目做出贡献,请阅读 CONTRIBUTING 指南