qaribou/immutable.php

不可变、高性能集合,非常适合函数式编程和内存密集型应用。

2.0.0 2019-10-09 15:39 UTC

This package is auto-updated.

Last update: 2024-09-10 02:02:32 UTC


README

不可变集合,非常适合函数式编程和内存密集型应用。在PHP7中运行速度尤其快。

基本用法

快速从简单数组加载

use Qaribou\Collection\ImmArray;
$polite = ImmArray::fromArray(['set', 'once', 'don\'t', 'mutate']);
echo $polite->join(' ');
// => "set once don't mutate"

使用回调映射

$yelling = $polite->map(function($word) { return strtoupper($word); });

echo <<<EOT
<article>
  <h3>A Wonderful List</h3>
  <ul>
    {$yelling->join('<li>', '</li>')}
  </ul>
</article>
EOT;

// => <article>
// =>   <h3>A Wonderful List</h3>
// =>   <ul>
// =>     <li>SET</li><li>ONCE</li><li>DON'T</li><li>MUTATE</li>
// =>   </ul>
// => </article>

使用回调排序

echo 'Os in front: ' .
    $yelling
        ->sort(function($word) { return (strpos('O', $word) === false) ? 1 : -1; })
        ->join(' ');
// => "Os in front: ONCE DON'T MUTATE SET"

切片

echo 'First 2 words only: ' . $polite->slice(0, 2)->join(' ');
// => "set once"

加载大型对象

// Big memory footprint: $fruits is 30MB on PHP5.6
$fruits = array_merge(array_fill(0, 1000000, 'peach'), array_fill(0, 1000000, 'banana'));

// Small memory footprint: only 12MB
$fruitsImm = ImmArray::fromArray($fruits);

// Especially big savings for slices -- array_slice() gives a 31MB object
$range = range(0, 50000);
$sliceArray = array_slice($range, 0, 30000);

// But this is a 192 _byte_ iterator!
$immSlice = ImmArray::fromArray($range)->slice(0, 30000);

过滤

// Yes, we have no bananas
$noBananas = $fruitsImm->filter(function($fruit) { return $fruit !== 'banana'; });

连接(即合并)

$ia = ImmArray::fromArray([1,2,3,4]);
$ib = ImmArray::fromArray([5,6,7,8]);

// Like slice(), it's just a little iterator in-memory
$ic = $ia->concat($ib);
// => [1,2,3,4,5,6,7,8]

减少

$fruits = ImmArray::fromArray(['peach', 'plum', 'orange']);

$fruits->reduce(function($last, $cur, $i) {
  return $last . '{"' . $i . '":' . $cur . '"},';
}, '"My Fruits: ');

// => My Fruits: {"0":"peach"},{"1":"plum"},{"2":"orange"},

查找

$fruits = ImmArray::fromArray(['peach', 'plum', 'banana', 'orange']);

$fruitILike = $fruits->find(function ($fruit) {
  return $fruit === 'plum' || $fruit === 'orange';
});

// => 'plum'

数组可访问

echo $fruits[1];
// => "plum"

可计数

count($fruits);
// => 3

可迭代

foreach ($fruits as $fruit) {
    $fruitCart->sell($fruit);
}

从任何Traversable对象加载

$vegetables = ImmArray::fromItems($vegetableIterator);

甚至可以序列化为json!

echo json_encode(
    ['name' => 'The Peach Pit', 'type' => 'fruit stand', 'fruits' => $noBananas]
);
// => {"name": "The Peach Pit", "type": "fruit stand", "fruits": ["peach", "peach", .....

安装

immutable.php可以通过composer和packagist获取。

composer require qaribou/immutable.php

原因

这个项目源于我对三个其他项目的热爱:Hack(http://hacklang.org)、immutable.js(https://fbdocs.cn/immutable-js/)和标准PHP库(SPL)数据结构(https://php.ac.cn/manual/en/spl.datastructures.php)。

  • Hack和immutable.js都表明,即使在非常松散类型化的语言中,使用不可变数据结构也是可能的,并且是实用的。
  • Hack语言引入了许多自己的集合,以及特殊的语法,这些在PHP中是不可用的。
  • SPL有一些技术上非常优秀、经过优化的数据结构,但在现实世界的应用中往往不太实用。

为什么我没有直接使用SplFixedArray?

SplFixedArray在底层实现得非常好,但实际使用起来往往有些痛苦。与标准数组(实际上只是可变大小的哈希表——我能想到的最可变的数据结构)相比,它可以节省大量的内存,尽管也许没有PHP7中那么大。

静态工厂方法

SPL数据结构都集中在继承方法上,但我发现Hacklang集合采用的组合方法要友好得多。实际上,Hack中的所有集合类都是final的,这意味着你必须使用它们自己构建自己的数据结构,所以我以同样的方式处理SPL。使用继承丢失的最大好处是fromArray方法,它在C中实现,速度相当快。

class FooFixed extends SplFixedArray {}
$foo = FooFixed::fromArray([1, 2, 3]);
echo get_class($foo);
// => "SplFixedArray"

所以,尽管静态类方法fromArray()是从FooFixed类中调用的,但我们的$foo根本不是FooFixed,而是一个SplFixedArray

然而,ImmArray使用组合方法,因此我们可以静态绑定工厂方法。

class FooFixed extends ImmArray {}
$foo = FooFixed::fromArray([1, 2, 3]);
echo get_class($foo);
// => "FooFixed"

现在,随着依赖注入和类型提示变得越来越流行,我们的数据结构可以以我们想要的类为对象构建,这比以往任何时候都更重要。更重要的是,在PHP中实现类似的fromArray()要比我们这里使用的C优化过的fromArray()慢得多。

事实上的标准数组函数

经典的PHP库拥有一堆常用、性能良好的但界面不一致的数组函数(例如array_map($callback, $array)array_walk($array, $callback))。处理这些函数可以被视为PHP的一个奇特的小魅力。真正的问题是,这些函数都有一个共同点:您的对象必须是数组。不是类似数组的、不是ArrayAccessible的、不是可迭代的、不是可遍历的等等,而是一个数组。通过在JavaScript和其他地方构建如此常见的函数,例如map()filter()join(),可以通过传递回调函数到旧的函数中,轻松地构建新的不可变数组。

$foo = ImmArray::fromArray([1, 2, 3, 4, 5]);
echo $foo->map(function($el) { return $el * 2; })->join(', ');
// => "2, 4, 6, 8, 10"

序列化为JSON

越来越多地,PHP不再用于庞大的、视图逻辑密集型的应用,而是作为一层薄数据层存在,用于提供业务逻辑和数据源之间的接口,并由客户端或远程应用消费。我发现我现在写的代码大部分只是简单地渲染为JSON,然后在浏览器中的React.js或ember应用中加载。为了对JavaScript开发者友好,发送数组作为数组,而不是需要使用大量Object.keys魔法的“类似数组”对象,是很重要的。例如:

$foo = SplFixedArray::fromArray([1, 2, 3]);
echo json_encode($foo);
// => {"0":1,"1":2,"2":3}

对于PHP开发者来说,内部逻辑是有意义的——毕竟,你是在编码属性,但在JavaScript中工作时不希望使用这种格式。JavaScript中的对象是无序的,所以你需要通过一个单独的计数器循环,将每个字符串属性名通过将计数器转换回字符串、进行属性查找,并在达到对象键的长度后结束循环来查找每个字符串属性名。这是一件愚蠢的PitA,我们经常不得不忍受,而我们都更愿意一开始就得到一个数组。例如:

$foo = ImmArray::fromArray([1, 2, 3]);
echo json_encode($foo);
// => [1,2,3]

不可变性

特殊的接口为我们提供了一个适当的层,以强制执行不可变性。虽然immutable.php数据结构实现了ArrayAccess,但尝试向它们推送或设置将会失败。

$foo = new ImmArray();
$foo[1] = 'bar';
// => PHP Warning:  Uncaught exception 'RuntimeException' with message 'Attempt
to mutate immutable Qaribou\Collection\ImmArray object.' in
/project/src/Collection/ImmArray.php:169

替代迭代器

PHP7

众所周知,在PHPNG之前,回调函数的速度非常慢,但一旦PHP7成为标准,immutable.php需要的以回调函数为主的函数式编程方法将会变得更快。例如,比较这个基本测试:

// Make 100,000 random strings
$bigSet = ImmArray::fromArray(array_map(function($el) { return md5($el); }, range(0, 100000)));

// Time the map function
$t = microtime(true);
$mapped = $bigSet->map(function($el) { return '{' . $el . '}'; });
echo 'map: ' . (microtime(true) - $t) . 's', PHP_EOL;

// Time the sort function
$t = microtime(true);
$bigSet->sort(function($a, $b) { return strcmp($a, $b); });
echo 'mergeSort: ' . (microtime(true) - $t) . 's', PHP_EOL;

在5.6上

map: 0.30895709991455s
mergeSort: 6.610347032547s

在7.0alpha2上

map: 0.01442813873291s
mergeSort: 0.58948588371277s

哇!在我的笔记本电脑上运行,运行map函数(执行回调函数)在PHP7上快了21倍。运行稳定的归并排序算法在PHP7上快了11倍。大地图和排序总是很昂贵,但PHP7将可能非常昂贵的300ms映射时间减少到更容易管理的14ms。