Useful Information

Pipeline and States

While some of Pipeline’s functions are stateless (e.g. Map and Expand), others (e.g. Take and Reduce) keep a state in some situations. Specifically, if $keepState (the second argument to __invoke() for every function) is true then these function will keep a state even after their generator has been fully iterated. As an example, Take will remember how many values it has returned:

<?php
use Webbhuset\Pipeline\Constructor as F;

$take = F::Take(3);

echo json_encode(iterator_to_array($take([1,2,3,4], true)));
echo "\n";
echo json_encode(iterator_to_array($take([5,6,7,8], true)));

/**
 * Output:
 *  [1,2,3]
 *  []
 */

Since $keepState defaults to false this is normally not something that you have to worry about when using Pipeline, and is mostly used internally by some of the Flow Functions.

States can also cause issues if multiple generators are created from these functions and iterated simultaneously (even if $keepState is false). This is most easily circumvented by using a Defer to build the functions separately for every input:

<?php
use Webbhuset\Pipeline\Constructor as F;

$take = F::Take(5);

$takeWithDefer = F::Defer(function () {
    return F::Take(5);
});

$input1 = range(0, 9);
$input2 = range(10, 19);

function loop($gen1, $gen2) {
    while ($gen1->valid() || $gen2->valid()) {
        if ($gen1->valid()) {
            echo 'Gen1: ' . $gen1->current() . "\n";
            $gen1->next();
        }
        if ($gen2->valid()) {
            echo 'Gen2: ' . $gen2->current() . "\n";
            $gen2->next();
        }
    }
}

$gen1 = $take($input1);
$gen2 = $take($input2);

echo "Without Defer, unexpected results:\n";
loop($gen1, $gen2);

$gen1 = $takeWithDefer($input1);
$gen2 = $takeWithDefer($input2);

echo "With Defer, expected results:\n";
loop($gen1, $gen2);

/**
 * Output:
 *  Without Defer, unexpected results:
 *  Gen1: 0
 *  Gen2: 10
 *  Gen1: 1
 *  Gen2: 11
 *  Gen1: 2
 *  Gen1: 3
 *  Gen1: 4
 *  Gen1: 5
 *  Gen1: 6
 *  Gen1: 7
 *  With Defer, expected results:
 *  Gen1: 0
 *  Gen2: 10
 *  Gen1: 1
 *  Gen2: 11
 *  Gen1: 2
 *  Gen2: 12
 *  Gen1: 3
 *  Gen2: 13
 *  Gen1: 4
 *  Gen2: 14
 */

Separating Flow and Logic

Just like when writing normal functions it is preferable to have multiple functions with descriptive names instead of adding everything into one function that does everything. Additionally this promotes separating functions responsible for the flow of data and functions responsible for manipulating data. Compare the following:

Using named functions:

<?php
class myFunctionBuilder
{
    public function buildMyFunction()
    {
        return [
            $this->mapRows(),
            $this->filterInvalid(),
            $this->insertToDatabase(),
        ];
    }

    protected function mapRows()
    {
        return F::Map(function ($value) {
            // ...
        });
    }

    protected function filterInvalid()
    {
        return F::Filter(function ($value) {
            // ...
        });
    }

    protected function insertToDatabase()
    {
        return F::Observe(function ($value) {
            // ...
        });
    }
}

Without named functions:

<?php
class myFunctionBuilder
{
    public function buildMyFunction()
    {
        return [
            F::Map(function ($value) {
                // ...
            }),
            F::Filter(function ($value) {
                // ...
            }),
            F::Observe(function ($value) {
                // ...
            }),
        ];
    }
}