xi/collections

PHP 5.3 的功能性强、不可变和可扩展的枚举和集合。

0.3.0 2012-12-14 08:42 UTC

This package is not auto-updated.

Last update: 2024-09-14 13:07:36 UTC


README

PHP 5.3 的功能性强、不可变和可扩展的枚举和集合。

设计理念

PHP 始终缺乏坚实的集合支持,绝大多数程序员只能满足于使用数组及其相关内置函数。随着 PHP 5.0 中 SPL 的引入和随后的 5.3 版本扩展,如果你想要的只是速度和针对特定用例的答案,那么现在有比以往更多的选择。然而,数组处理并没有比十年前有显著改进,API 的舒适度和便捷度与用铲子挑食晚餐差不多。

Xi Collections 致力于解决这个问题,并为你的工作流程注入大量函数性和声明性方面。这旨在使表达和理解处理对象或数据集合的过程更加清晰,让你工作更快,并交付更多自我文档化的代码。

设计原则

  • 对象不可变性。 集合的方法不操作集合的内容,而是返回一个新的集合。

  • API 可链性。 大多数操作将返回一个新的集合,除非是特定的 reduce 和大小信息。

  • 拥抱函数式编程。 集合 API 被选用来促进函数式工作流程。优先考虑包含 API 中的 PHP 中现有的与 FP 兼容的功能,并从其他语言和库中借用其他重要概念。

  • 即插即用可扩展性。 包含分离具体集合实现和 API 修改的装饰器。接口倾向于最小化而不是最大化,使新实现更容易。具体来说,PHP 的所有数组函数并不是内置的,但如果你需要,可以轻松使用。

示例

从命令式到函数式

让我们假设一个简单的循环,它会过滤和转换一组数据

public function getMatchingInterestingParts() {
	$result = array();
	foreach ($this->getFoos() as $key => $value) {
		if ($this->match($value)) {
			$result[$key] = $value->getInterestingParts();
		}
	}
	return $result;
}

以下是使用集合表达相同的代码

public function getMatchingInterestingParts() {
    return $this->getFoos()
        ->filter(function(Foo $foo) {
            return $this->matches($foo);
        })->map(function(Foo $foo) {
            return $foo->getInterestingParts();
        });
}

这段代码并不比之前的短,对于不熟悉函数式结构的人来说,可能更难处理。但是,它有几个有趣的特性。代码传达了它的意图更好 - 过滤值,然后映射结果,没有其他。这特别有利于考虑具有复杂转换集的代码。错误的空间更小;索引关联自动维护。这也意味着你可以专注于有趣的片段,而不是样板代码,这有助于阅读和编写代码。第三个好处是,你可以充分利用类型提示及其带来的安全性,这是简单 foreach 循环所缺乏的。

简化常见的访问模式

循环遍历数组最常见的一个用例是从每个项目收集成员访问或方法调用的结果。集合使得这变得容易。

public function getBarsByFoos() {
    $bars = array();
    foreach ($this->getFoos() as $key => $foo) {
        $bars[$key] = $foo->getBar();
    }
    return $bars;
}
// becomes
public function getBarsByFoos() {
    return $this->getFoos()->invoke('getBar');
}

public function getFooTrivialities() {
    $trivialities = array();
    foreach ($this->getFoos() as $key => $foo) {
        $trivialities[$key] = $foo->triviality;
    }
    return $trivialities;
}
// becomes
public function getFooTrivialities() {
    return $this->getFoos()->pick('triviality');
}

选择器甚至适用于数组(或实现 ArrayAccess 的对象),你不需要关心输入的类型。

检查复杂操作的中途步骤

假设你有一个数据根据复杂规则进行转换的管道。

public function getAliveQuxen() {
    return $this->getFoos()
        ->map($this->fromFooToBar)
        ->filter(function($bar) { return $bar->isAlive(); })
        ->map($this->fromBarToQux);
}

假设你进一步想要检查数据从一个步骤传递到另一个步骤的情况。这就是你需要引入临时变量的地方,如果代码是命令式的结构。使用集合,你只需要tap。它接受一个函数,该函数将集合的内容作为参数 - 只做调用函数的事情。

public function getAliveQuxen() {
    return $this->getFoos()
        ->map($this->fromFooToBar)
        ->filter(function($bar) { return $bar->isAlive(); })
        ->tap(function($bars) { $this->log($bars); })
        ->map($this->fromBarToQux);
}

阅读你的代码的人可以立即认出,在tap中的部分只是为了副作用而执行,它与本身的转换没有任何关系。如果我们对计算的各个单元感兴趣,我们也可以用each以类似的方式使用。

public function getAliveQuxen() {
    return $this->getFoos()
        ->map($this->fromFooToBar)
        ->filter(function($bar) { return $bar->isAlive(); })
        ->each(function($bar) { $this->logBar($bar); })
        ->map($this->fromBarToQux);
}

使用视图延迟计算

在某些情况下,你可能希望将某个集合暴露给消费者,但不确定该集合是否会被使用,而且生成一个集合可能是昂贵的。在这种情况下,你可以应用一个CollectionView,它是一组尚未应用于底层基本集合的转换操作。在访问时,将应用这些操作,并将结果值提供给消费者。

通过调用view将集合转换为一个由它本身支持的视图。尽可能将所有集合方法调用延迟执行。在访问任何枚举方法时,才会强制将视图转换为实际值。

public function getEnormouslyExpensiveCollection() {
    return $this->getStuff()->view()->map(function(Stuff $s) {
        return enormouslyExpensiveComputation($s);
    });
}

但是有一个警告。不能保证从延迟到严格的转换在每个CollectionView对象上恰好发生一次。如果你需要这样,你应该强制视图对象得到一个严格的视图。

即时使用扩展API

在任何PHP环境中,通常都存在一定数量的现有功能,用于以可遍历的格式处理数据。PHP本身有大量内置的数组函数,如果打算保持Collection API的简洁性,就不可能支持这些函数。随着PHP 5.4中引入特性,这可能会发生变化,但现在你必须想出手动使用这些函数的方法。这个设施的核心是apply。它接受一个函数,将某种转换应用于集合,然后将结果作为新的集合接收。

假设你想对值进行排序。这里是一个使用apply的方法。

public function getSortedFoos() {
    return $this->getFoos()
        ->apply(function($collection) {
            $foos = $collection->toArray();
            ksort($foos);
            return $foos;
        });
}

参数是一个集合,必须先将其转换为数组,然后才能被ksort接受。该函数也操作引用而不是值,因此需要临时变量。在这种情况下有一些冗余,但你很少会使用原始的PHP函数。如果你使用具有更合理的API的函数,例如接受可遍历对象而不是数组,则占用空间将小得多。例如,在这样一个虚构的场景中,对于ksort来说

public function getSortedFoos() {
    return $this->getFoos()
        ->apply('ksort');
}

API基础知识

集合有两个核心接口。Enumerable实现了一组仅依赖于可遍历性的集合操作。CollectionEnumerable操作扩展到更大的集合,包括返回其他集合的操作。这意味着集合可以转换成其他集合。

每个具体类都有一个静态的create方法,可以用来流畅地构造和访问集合。例如

ArrayCollection::create($values)->invoke('getBar')->each(function(Bar $bar) { $bar->engage(); });

以下是Enumerable和Collection提供的API的简要描述。对于更详细的信息,您需要查阅源代码。

Enumerable

元素检索

  • first:返回集合中的第一个元素
  • last:返回集合中的最后一个元素
  • find:返回满足给定谓词的第一个值

元素条件

  • exists:检查集合是否至少有一个元素满足给定的谓词
  • forAll:检查集合中所有元素是否都满足给定的谓词。
  • countAll:计算满足给定谓词的集合中元素的数量。

大小信息

  • count:计算集合中元素的数量。

归约

  • reduce:使用给定的回调从提供的初始值开始将集合的元素归约为一个单值。

调用

  • tap:使用此对象作为参数调用提供的回调。
  • each:对每个键值对执行一次操作。

集合

映射

  • map:对集合中的每个值-键对应用回调,并返回一个新的值,其中值被回调的返回值替换。
  • flatMap:对集合中的每个键值对应用回调,假设回调结果的值是可迭代的,并返回一个新的值,其中包含这些可迭代的值。
  • pick:获取一个从每个值中选择的键或成员属性的集合。
  • values:获取只包含此集合值的集合。
  • keys:获取一个以此集合的键作为值的集合。
  • invoke:通过在每个值上调用方法来映射此集合。
  • apply:从给定回调的输出创建一个新集合,该回调以此集合作为其参数。

子集合

  • take:创建一个新集合,其中包含此集合的前$number个元素。
  • rest:创建一个新集合,包含除了第一个元素之外的所有元素。
  • filter:创建一个集合,包含与此集合匹配的值的值。
  • filterNot:创建一个集合,包含与此集合不匹配的值的值。
  • unique:获取一个只包含此集合的唯一值的集合。

细分

  • partition:将集合分成两个集合;一个包含匹配给定谓词的元素,另一个包含不匹配的元素。
  • groupBy:根据给定的回调将集合中的值分组到嵌套集合中。

添加

  • concatenate:创建一个包含此集合和另一个集合元素的集合。
  • union:创建一个包含键值对的新集合,在$other集合中覆盖$this集合中的键值对。
  • flatten:展开嵌套数组和可遍历对象。
  • add:获取一个新集合,其中包含给定值和可选的键追加。

大小信息

  • isEmpty:检查集合是否为空。

排序

  • indexBy:使用给定的回调重新索引集合。
  • sortWith:获取使用给定比较函数排序的此集合。
  • sortBy:获取使用给定度量排序的此集合。

视图

  • view:提供一个集合,其中转换操作是按需应用的。

特定的归约

  • min:返回集合中的最小值。
  • max:返回集合中的最大值。
  • sum:返回集合中值的总和。
  • product:返回集合中值的乘积。

集合视图

  • force:将此视图强制转换为底层集合类型。

集合实现

  • ArrayCollection:由普通PHP数组支持的集合。
  • OuterCollection:集合的装饰器。可以轻松扩展以提供更多集合操作,而不锁定实现的具体细节。

运行单元测试

phpunit -c tests

TODO

  • 由SPL(SplFixedArray、SplDoublyLinkedList)支持的集合实现
  • 使用特性吗?