harmenjanssen / lazycollection
Functional iterator patterns using generators.
Requires
- php: >=7.0.0
Requires (Dev)
- phpunit/phpunit: ^5.7
This package is auto-updated.
Last update: 2024-11-17 18:59:46 UTC
README
What's this?
A LazyCollection
class that takes care of creating a generator for your collection.
Generators don't work with the standard map
, filter
and reduce
functions in PHP.
This is a shame of course because you lose a lot of handy abstractions. This package implements those methods lazily.
Consider the following example with traditional arrays:
$square = function($x) { return $x * $x; }; $isEven = function($x) { return $x % 2 === 0; }; $highestEvenSquare = array_filter( array_map( $square, range(30, 0) ), $isEven )[0];
This requires the entire range to be created, mapped to squares, and filtered before we can grab the first item.
With LazyCollection
, this could be implemented like this:
$lazyCollection = new LazyCollection\Examples\LazyArray(range(29, 0)); $lazyCollection->map($square) ->filter($isEven) ->first();
Of course, the initial range will be created in memory, but only the first two items would need to be squared and filtered.
Since the second item, 28, is a match to our filter function (28 * 28 results in the even number 784), first()
will just stop there.
The rest of the squares won't be computed.
It's easy to imagine a collection where every next item is computed on the spot, in which case the original array doesn't even have to be created in memory (for instance: search a large database or file until a fixed number of matches is found).
Installation
Install using Composer:
composer require harmenjanssen/lazycollection
Usage
all()
- This method returns the actual
Generator
:
$numbers = LazyCollection\Examples\Numbers::from(1); foreach ($numbers->take(100)->all() as $n) { echo $n . "\n"; } // will echo numbers 1 through 100
first()
- Take the first item of the collection.
take(int $amount): Subset
- Limits the collection to the given amount. Returns a new
LazyCollection
of typeSubset
. See example underall()
.
slice(int $start, int $length): Subset
- Take a subset of
$length
items of the collection starting at$start
.
$numbers = LazyCollection\Examples\Numbers::from(1); $fifty = $numbers->slice(50, 10); iterator_to_array($fifty); // [50, 51, ... 60]
map(callable $transformer): Mapped
- Add a mapping function to the collection. Returns a new
LazyCollection
of typeMapped
.
$numbers = LazyCollection\Examples\Numbers::from(1); $squares = $numbers->map(function($x) { return $x * $x; }); iterator_to_array($squares->take(10)); // [1, 4, 9, 16, 25... 100]
filter(callable $predicate): Filtered
- Add a filter function to the collection. Returns a new
LazyCollection
of typeFiltered
.
$numbers = LazyCollection\Examples\Numbers::from(1); $evenStevens = $numbers->filter(function($x) { return $x % 2 === 0; }); iterator_to_array($evenStevens->take(10)); // [2, 4, 6, 8, 10... 20]
reduce(callable $reducer, $seed)
- Reduce the collection to a single value.
$numbers = LazyCollection\Examples\Numbers::from(1)->take(10); $sum = $numbers->reduce(function($a, $b) { return $a + $b; }, 0); // 55
Implement your own lazy collections
Create a subclass of LazyCollection
, implementing the 4 required methods:
start()
- Returns the initial iteration. Note that this is not the same as an iteration's value, nor does it have to be numeric. You determine how you iterate your collection and what data you need to progress through it. See the Fibonacci example for a collection that uses an array to track progress through the collection
next($iteration)
- Returns the next iteration based on the previous iteration.
Note that the return type and the type of its parameter are the same as those of
start()
, most likely.
done(): bool
- Wether iteration is done.
value($iteration)
- Return a value based on the given iteration.
Infinite collections
Infinite collection are elegant solutions to computational patterns.
Both the Numbers sequence and the Fibonacci sequence are examples of infinite collections.
You can project a range of values, but there's no natural end.
Even though iterating over these collections is no problem, you have to make sure you bring iteration to a halt yourself, or you'll run out of memory.
take()
is a handy method to use in this case.
$fibonacci = new LazyCollection\Examples\Fibonacci; iterator_to_array($fibonacci->take(10)->all()); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Also consider this pitfall: when filtering an infinite collection by a predicate that matches nothing, the filter will not return an empty collection, as it would with regular arrays. It will run out of memory since it never ends.
// This will never resolve: $fibonacci = new LazyCollection\Examples\Fibonacci; $fibonacci->filter('is_string')->take(10);
Note also that you cannot use iterator_to_array
to flatten the iterator into an array. Again, this would never end.
Todos
- Implement
until()
- Implement
find()
Inspiration
I picked up the concept of functional iterators from the wonderful book Javascript Allongé, by Reginald Braithwaite.
Lots of interesting languages support the concept of lazy (possibly infinite) sequences, like Haskell and Clojure.