iter icon indicating copy to clipboard operation
iter copied to clipboard

Feature: fluent interface

Open SamMousa opened this issue 6 years ago • 5 comments

Would it be nice to have some kind of fluent interface? That would allow for really readable code like:

toIter([1, 2, 3, 4, 5, 6])
    ->filter(function($v) { return $v % 2 === 0; })
    ->slice(0, 1)

I think it can be implemented relatively simply:

class Test implements \IteratorAggregate {
    private $iterator;

    public function __construct(iterable $iterator)
    {
        $this->iterator = \iter\toIter($iterator);
    }

    public function getIterator()
    {
        return $this->iterator;
    }

    public function filter(\Closure $predicate) {
        return new self(\iter\filter($predicate, $this));
    }
}

What do you think?

I could implement this and if we alter toIter to return the new type, it wouldn't even break backwards compatibility since people are expecting a generator.

SamMousa avatar Jul 26 '17 16:07 SamMousa

Am I getting it right? toIter should return an instance of Test and Test should contain all operations (including slice which is missing in the example)?

maiermic avatar Mar 15 '18 17:03 maiermic

You are right

SamMousa avatar Mar 15 '18 18:03 SamMousa

This could be implemented as an external library (not everyone might like to use it). Here is a working example (as you proposed):

class FluentIter implements \IteratorAggregate
{
    private $iterator;

    public function __construct($iterator)
    {
        $this->iterator = \iter\toIter($iterator);
    }

    public static function from($iterator)
    {
        return new static($iterator);
    }

    public function getIterator()
    {
        return $this->iterator;
    }

    public function filter(\Closure $predicate)
    {
        return new static(\iter\filter($predicate, $this));
    }

    public function slice($start, $length = INF)
    {
        return new static(\iter\slice($this, $start, $length));
    }
}

$result = FluentIter::from([1, 2, 3, 4, 5, 6])
    ->filter(function ($v) {
        return $v % 2 === 0;
    })
    ->slice(0, 1);

echo json_encode(\iter\toArray($result));

If you like to add custom operators you can extend this class. Its important that you use new static(...) instead of new self(...). Otherwise, the custom operators are not available. Here is an example that adds custom operators:

class ExtendedFluentIter extends FluentIter
{
    public function multiplyBy($factor)
    {
        return new static(\iter\map(\iter\fn\operator('*', $factor), $this));
    }

    public function toJson()
    {
        return json_encode(\iter\toArray($this));
    }
}

echo ExtendedFluentIter::from([1, 2, 3, 4, 5, 6])
    ->multiplyBy(2)
    ->toJson();

You might use traits for each operator:

trait MultiplyOperator {
    public function multiplyBy($factor)
    {
        return new static(\iter\map(\iter\fn\operator('*', $factor), $this));
    }
}

trait ToJsonOperator {
    public function toJson()
    {
        return json_encode(\iter\toArray($this));
    }
}

class ExtendedFluentIter extends FluentIter
{
    use MultiplyOperator, ToJsonOperator;
}

Thereby, you can easily add operators from different sources:

// ----------------------------------
// libraries outside of my project
// ----------------------------------

// operators introduced by a library foo
trait LibFooOperators {
    use LibFooOperatorA, LibFooOperatorB;
}

// operators introduced by a library bar
trait LibBarOperators {
    use LibBarOperatorA, LibBarOperatorB;
}

// ----------------------------------
// in my project
// ----------------------------------

// project specific operators
trait MyOperators { ... }

class MyExtendedFluentIter extends FluentIter
{
    use LibFooOperators, LibBarOperators, MyOperators;
}

maiermic avatar Mar 15 '18 18:03 maiermic

Maybe it is better to make the contract more explicit by defining an Operator trait that defines the required functions:

trait Operator
{
    /**
     * @param $iterator
     * @return static
     */
    public abstract static function from($iterator);

    public abstract function getIterator();
}

trait MultiplyOperator
{
    use Operator;

    public function multiplyBy($factor)
    {
        return $this->from(\iter\map(\iter\fn\operator('*', $factor), $this->getIterator()));
    }
}

trait ToJsonOperator
{
    use Operator;

    public function toJson()
    {
        return json_encode(\iter\toArray($this->getIterator()));
    }
}

You shouldn't use other concrete operator traits in another operator because it might cause conflicts when using them:

trait DoubledOperator
{
    use MultiplyOperator; // DON'T use other operator because it might cause conflicts

    public function doubled() {
        return $this->multiplyBy(2);
    }
}

class ExtendedFluentIter extends FluentIter
{
    use DoubledOperator, MultiplyOperator, ToJsonOperator; // PHP Fatal error:  Trait method multiplyBy has not been applied, because there are collisions with other trait methods
}

Instead define the abstract signature of the required operator:

trait DoubledOperator
{
    /**
     * @param $factor
     * @return static
     */
    public abstract function multiplyBy($factor); // DO define abstract signature of required operator

    /**
     * Multiply each value by 2.
     * @return static
     */
    public function doubled() {
        return $this->multiplyBy(2);
    }
}

maiermic avatar Mar 15 '18 23:03 maiermic

Until it's implemented here, check out how we did this for nextbigsoundinc/[email protected]:

$integers = function () {
	for ($int = 1; true; $int++) {
		yield $int;
	}
};

Dash\chain($integers())
	->filter('Dash\isOdd')
	->take(3)
	->reverse()
	->value();
// === [5, 3, 1]

mpetrovich avatar Aug 20 '18 16:08 mpetrovich