Giter Club home page Giter Club logo

immutable.php's Introduction

immutable.php

Immutable collections, well-suited for functional programming and memory-intensive applications. Runs especially fast in PHP7.

Basic Usage

Quickly load from a simple array

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

Map with a callback

$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>

Sort with a callback

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

Slice

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

Load big objects

// 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);

Filter

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

Concat (aka merge)

$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]

Reduce

$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"},

Find

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

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

// => 'plum'

Array accessible

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

Countable

count($fruits);
// => 3

Iterable

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

Load from any Traversable object

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

Even serialize back as json!

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

Install

immutable.php is available on composer via packagist.

composer require qaribou/immutable.php

Why

This project was born out of my love for 3 other projects: Hack (http://hacklang.org), immutable.js (https://facebook.github.io/immutable-js/), and the Standard PHP Library (SPL) datastructures (http://php.net/manual/en/spl.datastructures.php).

  • Both Hack and immutable.js show that it's both possible, and practical to work with immutable data structures, even in a very loosely-typed language
  • The Hack language introduced many collections of its own, along with special syntax, which are unavailable in PHP.
  • SPL has some technically excellent, optimized datastructures, which are often impractical in real world applications.

Why didn't I just use SplFixedArray directly?

The SplFixedArray is very nicely implemented at the low-level, but is often somewhat painful to actually use. Its memory savings vs standard arrays (which are really just variable-sized hashmaps -- the most mutable datastructure I can think of) can be enormous, though perhaps not quite as big a savings as it will be once PHP7 gets here. By composing an object with the SplFixedArray, we can have a class which solves the usability issues, while maintaining excellent performance.

Static-Factory Methods

The SPL datastructures are all very focused on an inheritance-approach, but I found the compositional approach taken in hacklang collections to be far nicer to work with. Indeed, the collections classes in hack are all final, implying that you must build your own datastructures composed of them, so I took the same approach with SPL. The big thing you miss out on with inheritance is the fromArray method, which is implemented in C and quite fast, however:

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

So you can see that while the static class method fromArray() was called from a FooFixed class, our $foo is not a FooFixed at all, but an SplFixedArray.

ImmArray, however, uses a compositional approach so we can statically bind the factory methods:

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

Now that dependency injection, and type-hinting in general, are all the rage, it's more important than ever that our datastructures can be built as objects for the class we want. It's doubly important, because implementing a similar fromArray() in PHP is many times slower than the C-optimized fromArray() we use here.

De-facto standard array functions

The good ol' PHP library has a pile of often useful, generally well-performing, but crufty array functions with inconsistent interfaces (e.g. array_map($callback, $array) vs array_walk($array, $callback)). Dealing with these can be considered one of PHP's quirky little charms. The real problem is, these functions all have one thing in common: your object must be an array. Not arraylike, not ArrayAccessible, not Iterable, not Traversable, etc., but an array. By building in functions so common in JavaScript and elsewhere, e.g. map(), filter(), and join(), one can easily build new immutable arrays by passing a callback to the old one.

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

Serialize as JSON

More and more, PHP is being used less for bloated, view-logic heavy applications, and more as a thin data layer that exists to provide business logic against a datasource, and be consumed by a client side or remote application. I've found most of what I write nowadays simply renders to JSON, which I'll load in a React.js or ember application in the browser. In the interest of being nice to JavaScript developers, it's important to send arrays as arrays, not "arraylike" objects which need to have a bunch of Object.keys magic used on them.e.g.

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

The internal logic makese sense to a PHP dev here -- you're encoding properties, after all, but this format is undesirable when working in JS. Objects in js are unordered, so you need to loop through a separate counter, and lookup each string property-name by casting the counter back to string, doing a property lookup, and ending the loop once you've reached the length of the object keys. It's a silly PitA we often have to endure, when we'd much rather get back an array in the first place. e.g.

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

Immutability

A special interface gives us an appropriate layer to enforce immutability. While the immutable.php datastructures implement ArrayAccess, attempts to push or set to them will fail.

$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

Alternative Iterators

PHP7

It's well-known that callbacks are incredibly slow pre-PHPNG days, but once PHP7 becomes the standard the callback-heavy approach to functional programming needed by immutable.php will become far faster. For example, compare this basic test:

// 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;

On 5.6:

map: 0.30895709991455s
mergeSort: 6.610347032547s

On 7.0alpha2:

map: 0.01442813873291s
mergeSort: 0.58948588371277s

Holy moly! Running on my laptop, running the map function (which executes a callback) is 21x faster on PHP7. Running the stable mergesort algorithm is 11x faster on PHP7. Big maps and sorts will always be expensive, but PHP7 drops what may be a prohibitively expensive 300ms map, to a much more manageable 14ms.

immutable.php's People

Contributors

hansott avatar jarjak avatar jkoudys avatar localheinz avatar royopa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

immutable.php's Issues

Immutability and loss of information

Hi @jkoudys ,

Thanks a lot for sharing your super-accurate work.
I have a question concerning Immutability. Presently, you make your collections immutable by calling return new static(SplFixedArray $fixedArray); at each end of mutator methods.

This works perfectly as long as the collection itself as no more property than the content of the $fixedArray parameter.

For example if I extend ImmArray this way :

class MyLovelyCollection extends ImmArray
{
    protected $feeling = 'loneliness';
    
    public function __construct($fixedArray, $feeling)
    {
        $this->feeling = $feeling;
        parent::__construct($fixedArray);
    }
    
    public function getFeeling()
    {
        return $this->feeling;
    }
}

And then call something like

$faces = new MyLovelyCollection([':)', ':/', ':D', 'XD'], 'happiness');

echo $faces->getFeeling() // => 'happiness'

$faces->filter(function($face) {
    return ! preg_match("#[^/]$#", $face);
});

echo $faces->getFeeling() // => 'loneliness'

Because of this, I cannot use together the quality of your iterable ImmArray (it has no point inside a composition) and some properties (mutable or not) that I would attach to. Simple things like composition becomes super hard due to it.

So, more pragmatically, my question is "Why don't do clone $collection; return $collection->doSomething(); instead of return new static(...);?".

Thanks in advance for your time,
Jean

Not compatible with PHP >=7.1.0 as claimed in composer package (aka incompatible with PHP >= 8.0)

I am getting the following error when using ImmArray:

Call to undefined method SplFixedArray::rewind()

An internet search revealed this change notice of PHP 8.0:

  . SplFixedArray is now an IteratorAggregate and not an Iterator.
    SplFixedArray::rewind(), ::current(), ::key(), ::next(), and ::valid()
    have been removed. In their place, SplFixedArray::getIterator() has been
    added. Any code which uses explicit iteration over SplFixedArray must now
    obtain an Iterator through SplFixedArray::getIterator(). This means that
    SplFixedArray is now safe to use in nested loops.

(source: https://github.com/php/php-src/blob/e4e2541c1a90daf708ab1ee4d44f2ab2883ce84b/UPGRADING#L515-L520)

Therefore I deduct that immutable.php is not compatible with PHP 8.0 and above, as claimed in package metadata.
(I am using the package via composer. there ">=7.1.0" is claimed)

Tests are failing

Running the tests on a fresh clone currently fails:

$ phpunit --configuration tests/phpunit.xml

PHPUnit 4.8.14 by Sebastian Bergmann and contributors.

.......PHP Fatal error:  Allowed memory size of 52428800 bytes exhausted (tried to allocate 32 bytes) in /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php on line 100
PHP Stack trace:
PHP   1. {main}() /Users/am/.composer/vendor/phpunit/phpunit/phpunit:0
PHP   2. PHPUnit_TextUI_Command::main() /Users/am/.composer/vendor/phpunit/phpunit/phpunit:47
PHP   3. PHPUnit_TextUI_Command->run() /Users/am/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:100
PHP   4. PHPUnit_TextUI_TestRunner->doRun() /Users/am/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:149
PHP   5. PHPUnit_Framework_TestSuite->run() /Users/am/.composer/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:440
PHP   6. PHPUnit_Framework_TestSuite->run() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestSuite.php:747
PHP   7. PHPUnit_Framework_TestCase->run() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestSuite.php:747
PHP   8. PHPUnit_Framework_TestResult->run() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:724
PHP   9. PHPUnit_Framework_TestCase->runBare() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestResult.php:612
PHP  10. PHPUnit_Framework_TestCase->runTest() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:768
PHP  11. ReflectionMethod->invokeArgs() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:909
PHP  12. ImmArrayTest->testLoadBigSet() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:909
PHP  13. array_map() /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100
PHP  14. ImmArrayTest->{closure:/Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100-100}() /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100
PHP  15. md5() /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100

Fatal error: Allowed memory size of 52428800 bytes exhausted (tried to allocate 32 bytes) in /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php on line 100

Call Stack:
    0.0002     238216   1. {main}() /Users/am/.composer/vendor/phpunit/phpunit/phpunit:0
    0.0028     668264   2. PHPUnit_TextUI_Command::main() /Users/am/.composer/vendor/phpunit/phpunit/phpunit:47
    0.0028     668904   3. PHPUnit_TextUI_Command->run() /Users/am/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:100
    0.0471    4257656   4. PHPUnit_TextUI_TestRunner->doRun() /Users/am/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:149
    0.0516    4440752   5. PHPUnit_Framework_TestSuite->run() /Users/am/.composer/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:440
    0.0520    4456216   6. PHPUnit_Framework_TestSuite->run() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestSuite.php:747
    0.0596    4744928   7. PHPUnit_Framework_TestCase->run() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestSuite.php:747
    0.0596    4744928   8. PHPUnit_Framework_TestResult->run() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:724
    0.0596    4745936   9. PHPUnit_Framework_TestCase->runBare() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestResult.php:612
    0.0597    4762808  10. PHPUnit_Framework_TestCase->runTest() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:768
    0.0597    4763568  11. ReflectionMethod->invokeArgs() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:909
    0.0597    4763688  12. ImmArrayTest->testLoadBigSet() /Users/am/.composer/vendor/phpunit/phpunit/src/Framework/TestCase.php:909
    0.0855   34062136  13. array_map() /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100
    0.2867   52376512  14. ImmArrayTest->{closure:/Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100-100}() /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100
    0.2867   52376560  15. md5() /Users/am/Sites/jkoudys/immutable.php/tests/Collections/ImmArrayTest.php:100

Related to #4.

Use item index in map() callback

I'd like to have the item index available during mapping. If you want, I can create a pull request. For backwards compatibility, index can be used as a second argument of the callback.

Build SliceIterator for more efficient slices

To be javascript-like, the original implementation made a slice as a full clone. However, this is only necessary in JS specifically because an array is mutable. On an immutable array, it makes more sense to define an ArrayAccess, JsonSerializable LimitIterator to simply access a subset of the target ImmArray. It's not going anywhere in memory, so no need to clone when we can simply iterate at an offset.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.