Skip to content

Commit

Permalink
Merge pull request #3 from xi-project/views
Browse files Browse the repository at this point in the history
Views
  • Loading branch information
puppe0 committed Jul 3, 2012
2 parents 7067b54 + 7b8e292 commit 343b838
Show file tree
Hide file tree
Showing 22 changed files with 671 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/nbproject
/.idea
/tests/phpunit.xml
/tests/coverage
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ One of the most common use cases for looping over an array is collecting the res

Picking even works for arrays (or objects implementing ArrayAccess) as well, and you don't need to care about which type the input is.

## Inspect intermediate steps of complex operations
## Inspecting intermediate steps of complex operations

Suppose you have a pipeline where data is transformed according to complex rules.

Expand Down Expand Up @@ -108,6 +108,20 @@ A reader of your code will be able to immediately recognize that the part in `ta
->map($this->fromBarToQux);
}

## Delaying computation using views

In some cases you may wish to expose a certain Collection to a consumer, but are not certain whether the Collection is going to be used, and generating one is potentially costly. In such a case you can apply a CollectionView, which is a set of transformation operations that haven't yet been applied to an underlying base Collection. Upon access, the operations will be applied and the resulting values provided to the consumer.

A Collection is transformed into a view backed by itself by invoking `view`. Best effort is made to apply all Collection method calls lazily. Forcing the view into actual values happens when accessing any Enumerator methods.

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

There's a caveat, however. It is not guaranteed that the transformation from lazy to strict should happen exactly once per CollectionView object. If you need that, you should `force` the view object to get a strict one.

## Using an extended API on the fly

In any given PHP environment there tends to be an amount of existing functionality around for processing data in a Traversable format. PHP itself has a plethora of built-in array functions that aren't feasible to support in the Collection API if it is supposed to be kept minimal. This can potentially change with the introduction of traits in PHP 5.4, but for now you'll have to figure out ways to use these functions manually. At the core of this facility is `apply`. It accepts a function that applies a transformation of some kind to the collection, the result of which is taken in as a new collection.
Expand Down Expand Up @@ -152,10 +166,11 @@ Below is a short description of the APIs provided by Enumerable and Collection.

## Collection

`view`: Provides a Collection where transformer operations are applied lazily
`apply`: Creates a new Collection of this type from the output of a given callback that takes this Collection as its argument
`take`: Creates a new Collection with up to $number first elements from this one
`map`: Applies a callback for each value-key-pair in the Collection and returns a new one with values replaced by the return values from the callback
`filter`: Creates a collection with the values of this collection that match a given predicate
`filter`: Creates a Collection with the values of this collection that match a given predicate
`concatenate`: Creates a Collection with elements from this and another one
`union`: Creates a Collection with key-value pairs in the `$other` Collection overriding ones in `$this` Collection
`values`: Get a Collection with just the values from this Collection
Expand All @@ -166,8 +181,14 @@ Below is a short description of the APIs provided by Enumerable and Collection.
`invoke`: Map this Collection by invoking a method on every value
`flatten`: Flatten nested arrays and Traversables
`unique`: Get a Collection with only the unique values from this one
`sortWith`: Get this Collection sorted with a given comparison function
`sortBy`: Get this Collection sorted with a given metric

## CollectionView

## Most relevant Collection implementations
`force`: Coerces this view back into the underlying Collection type

## Collection implementations

`ArrayCollection`: Basic Collection backed by a plain PHP array.
`OuterCollection`: A decorator for a Collection. Can easily be extended to provide more collection operations without locking down the implementation specifics.
Expand All @@ -180,5 +201,5 @@ Below is a short description of the APIs provided by Enumerable and Collection.
# TODO

- Collection implementations backed by SPL (SplFixedArray, SplDoublyLinkedList?)
- Once PHP 5.4 comes about, implementations can be significantly simplified using traits
- The option to lazily apply some of the available transformations
- Once PHP 5.4 is feasible to use, implementations can be significantly simplified using traits

33 changes: 33 additions & 0 deletions library/Xi/Collections/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public static function create($elements);
*/
public static function getCreator();

/**
* Provides a collection where transformer operations are applied lazily
*
* @return CollectionView
*/
public function view();

/**
* Creates a new Collection of this type from the output of a given callback
* that takes this Collection as its argument.
Expand Down Expand Up @@ -101,6 +108,16 @@ public function values();
*/
public function keys();

/**
* Applies a callback for each key-value-pair in the Collection assuming
* that the callback result value is iterable and returns a new one with
* values from those iterables. Does not maintain index associations.
*
* @param callback($value, $key) $callback
* @return Collection
*/
public function flatMap($callback);

/**
* Reindex the Collection using a given callback
*
Expand Down Expand Up @@ -148,4 +165,20 @@ public function flatten();
* @return Collection
*/
public function unique($strict = true);

/**
* Get a new Collection sorted with a given comparison function
*
* @param callback($a, $b) $comparator
* @return Collection
*/
public function sortWith($comparator);

/**
* Get a new Collection sorted with a given metric
*
* @param callback($value, $key) $metric
* @return Collection
*/
public function sortBy($metric);
}
20 changes: 20 additions & 0 deletions library/Xi/Collections/Collection/AbstractCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public static function getCreator()
{
return Functions::getCallback(get_called_class(), 'create');
}

public function view()
{
return new SimpleCollectionView($this, static::getCreator());
}

public function apply($callback)
{
Expand Down Expand Up @@ -97,6 +102,11 @@ public function keys()
return static::create($results);
}

public function flatMap($callback)
{
return $this->apply(Functions::flatMap($callback));
}

public function indexBy($callback)
{
return $this->apply(Functions::indexBy($callback));
Expand Down Expand Up @@ -126,4 +136,14 @@ public function unique($strict = true)
{
return $this->apply(Functions::unique($strict));
}

public function sortWith($comparator)
{
return $this->apply(Functions::sortWith($comparator));
}

public function sortBy($metric)
{
return $this->apply(Functions::sortBy($metric));
}
}
23 changes: 22 additions & 1 deletion library/Xi/Collections/Collection/ArrayCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public static function getCreator()
{
return Functions::getCallback(get_called_class(), 'create');
}

public function view()
{
return new SimpleCollectionView($this, static::getCreator());
}

public function apply($callback)
{
Expand Down Expand Up @@ -106,6 +111,11 @@ public function keys()
return static::create(array_keys($this->_elements));
}

public function flatMap($callback)
{
return $this->apply(Functions::flatMap($callback));
}

public function indexBy($callback)
{
return $this->apply(Functions::indexBy($callback));
Expand Down Expand Up @@ -134,11 +144,22 @@ public function flatten()
public function unique($strict = true)
{
if (false === $strict) {
return static::create(array_unique($this->_elements));
// array_unique can't check for strict uniqueness
return static::create(array_unique($this->_elements, SORT_REGULAR));
}
return $this->apply(Functions::unique($strict));
}

public function sortWith($comparator)
{
return $this->apply(Functions::sortWith($comparator));
}

public function sortBy($metric)
{
return $this->apply(Functions::sortBy($metric));
}

/**
* @return ArrayCollection
*/
Expand Down
20 changes: 20 additions & 0 deletions library/Xi/Collections/Collection/OuterCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public static function getCreator()
return Functions::getCallback(get_called_class(), 'create');
}

public function view()
{
return new SimpleCollectionView($this, static::getCreator());
}

public function apply($callback)
{
return static::create($this->collection->apply($callback));
Expand Down Expand Up @@ -92,6 +97,11 @@ public function keys()
return static::create($this->collection->keys());
}

public function flatMap($callback)
{
return static::create($this->collection->flatMap($callback));
}

public function indexBy($callback)
{
return static::create($this->collection->indexBy($callback));
Expand Down Expand Up @@ -121,4 +131,14 @@ public function unique($strict = true)
{
return static::create($this->collection->unique($strict));
}

public function sortWith($comparator)
{
return static::create($this->collection->sortWith($comparator));
}

public function sortBy($metric)
{
return static::create($this->collection->sortBy($metric));
}
}
134 changes: 134 additions & 0 deletions library/Xi/Collections/Collection/SimpleCollectionView.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php
namespace Xi\Collections\Collection;
use Xi\Collections\Util;
use Xi\Collections\CollectionView;

class SimpleCollectionView extends SimpleCollection implements CollectionView
{
/**
* @var callback(Traversable) $creator
*/
protected $creator;

/**
* @param Traversable $traversable
* @param callback(Traversable) $creator optional
*/
public function __construct($traversable, $creator = null)
{
$this->traversable = $traversable;
if (null === $creator) {
$creator = SimpleCollection::getCreator();
}
$this->creator = $creator;
}

public function force()
{
$creator = $this->creator;
return $creator($this);
}

protected function lazy($iterator)
{
return new static($iterator, $this->creator);
}

public function apply($callback)
{
$self = $this;
return $this->lazy($this->getLazyIteratorFor(function() use($self, $callback) {
return $callback($self);
}));
}

public function take($number)
{
if (0 >= $number) {
return $this->lazy(new \EmptyIterator);
}
return $this->lazy(new \LimitIterator($this->getIterator(), 0, $number));
}

public function map($callback)
{
return $this->lazy($this->getMapIteratorFor($this->getIterator(), $callback));
}

public function filter($predicate = null)
{
if (null === $predicate) {
$predicate = function($value) {
return !empty($value);
};
}

return $this->lazy($this->getFilterIteratorFor($this->getIterator(), $predicate));
}

public function concatenate($other)
{
$iterator = new \AppendIterator;
$iterator->append($this->getIterator());
$iterator->append($this->getIteratorFor($other));
return $this->lazy($this->getReindexIteratorFor($iterator));
}

public function union($other)
{
$iterator = new \AppendIterator;
$iterator->append($this->getIterator());
$iterator->append($this->getIteratorFor($other));
return $this->lazy($this->getLazyIteratorFor(function() use($iterator) {
return iterator_to_array($iterator);
}));
}

public function flatMap($callback)
{
return $this->lazy($this->getReindexIteratorFor($this->getFlatMapIteratorFor($this->getIterator(), $callback)));
}

public function values()
{
return $this->lazy($this->getReindexIteratorFor($this->getIterator()));
}

public function keys()
{
return $this->lazy($this->getReindexIteratorFor($this->getMapIteratorFor(
$this->getIterator(),
function($v, $k) { return $k; }
)));
}

protected function getIteratorFor($other)
{
return Util\Functions::getIterator($other);
}

protected function getLazyIteratorFor($callback)
{
return new Util\LazyIterator($callback);
}

protected function getReindexIteratorFor($iterator)
{
return new Util\ReindexIterator($iterator);
}

protected function getFlatMapIteratorFor($iterator, $callback)
{
return new \RecursiveIteratorIterator(new Util\FlatMapIterator($iterator, $callback));
}

protected function getMapIteratorFor($iterator, $callback)
{
return new Util\CallbackMapIterator($iterator, $callback);
}

protected function getFilterIteratorFor($iterator, $callback)
{
return new Util\CallbackFilterIterator($iterator, $callback);
}
}
Loading

0 comments on commit 343b838

Please sign in to comment.