qaribou / immutable.php
不可变、高性能集合,非常适合函数式编程和内存密集型应用。
Requires
- php: >=7.1.0
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^5.0
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。