iter icon indicating copy to clipboard operation
iter copied to clipboard

First implementation of fluent interface

Open SamMousa opened this issue 5 years ago • 14 comments

First draft of #45

SamMousa avatar Mar 25 '19 14:03 SamMousa

Some ideas that could be implemented:

  • [ ] Early error checking; since we don't use generators we can often validate params before iteration starts.
  • [ ] Have toIter() return FluentIterator or add toFluentIterator()
  • [ ] Add iterator creators like range() and repeat() as static constructors

SamMousa avatar Mar 25 '19 15:03 SamMousa

I'm :-1: here, having a fluent interface would be annoying when you need to mix function from this package with your own functions (or functions from other packages).

I think instead we should provide curried versions of the functions and a pipe/flow function to allow chaining any list of function.

https://medium.com/making-internets/why-using-chain-is-a-mistake-9bc1f80d51ba explains in details (in javascript but some problems apply to PHP to) why such an interface comes with lots of problems.

jvasseur avatar Mar 26 '19 14:03 jvasseur

@jvasseur can you be more specific?

I'm -1 here, having a fluent interface would be annoying when you need to mix function from this package with your own functions (or functions from other packages).

Can you give an example, things like apply, or map should allow you to mix this with your own or third party functions easily. Also the intermediate object itself is an iterable, so you could pass it to anything that takes an iterable.

I think instead we should provide curried versions of the functions and a pipe / flow function to allow chaining any list of function.

Could you give a syntax example of how you envision this? I'm definitely open to suggestions.

https://medium.com/making-internets/why-using-chain-is-a-mistake-9bc1f80d51ba explains in details (in javascript but some problems apply to PHP to) why such an interface comes with lots of problems.

Could you specify which problems you think apply to PHP?

For me this is just a small wrapper that allows me to create more readable code without taking away any of the flexibility, I'm not proposing this implementation replaces the functional approach...

SamMousa avatar Mar 26 '19 14:03 SamMousa

@SamMousa I tend to agree with @jvasseur , I think the only way you could do fluent collection like stuff would be if you provided macros like laravel's collection package to allow patching in, but that comes at the cost of discoverability.

The issue he's referring to is, let's say you built your own function like chunkBy(callable $predicate) with the fluent version, you have no way of accessing that function in the chain. You'd have to provide the ability to like monkey patch the Iter class to allow arbitrary additions.

The other minor issue is that any new functions/changes need to be kept in sync and tested

Regrading iter vs functional syntax, here are some examples:

https://github.com/krakphp/fn - this is an example of what you can do with curried functions and compose/pipe chains.

and here's a syntax comparison (as shown in the article as well)

<?php

// imperative
$values = [1,2,3];
$values = map(function() {}, $values);
$values = filter(function() {}, $values);

// imperative chained
$values = filter(
    function() {}, 
    map(
        function() {}, 
        [1,2,3]
    )
);

// functional curried w/ compose
$values = compose(
    filter(function() {}), 
    map(function() {})
)([1,2,3]);

// functional curried w/ pipe
$values = pipe(
    map(function() {}),
    filter(function() {})
)([1,2,3]);


// fluent chaining
$values = iter([1,2,3])
    ->map(function() {})
    ->filter(function() {});

ragboyjr avatar Mar 26 '19 15:03 ragboyjr

@jvasseur can you be more specific?

I'm -1 here, having a fluent interface would be annoying when you need to mix function from this package with your own functions (or functions from other packages).

Can you give an example, things like apply, or map should allow you to mix this with your own or third party functions easily. Also the intermediate object itself is an iterable, so you could pass it to anything that takes an iterable.

What I mean is if you have your function myFunction that you want to put in the chain like this:

$fluent
    ->filter(...)
    ->myFunction()
    ->map(...)

jvasseur avatar Mar 26 '19 15:03 jvasseur

@ragboyjr thanks for the examples!

I'm not sure I fully agree with the reasoning though.

The issue he's referring to is, let's say you built your own function like chunkBy(callable $predicate) with the fluent version, you have no way of accessing that function in the chain. You'd have to provide the ability to like monkey patch the Iter class to allow arbitrary additions.

Because the fluent version is iterable itself you can just pass it into your function.

$fluent = new Fluent(['a','b']);

$result = chunkBy($fluent->map(...)->slice(1, 6))

Admittedly, you lose the syntactic sugar a bit. It would however by simple to extend the syntax like this:

$fluent = new Fluent(['a','b']);
$fluent->map(...)
    ->slice(1, 6)
    ->via($chunkBy)
    ->map(...);

The issue with PHP is that you cannot reference a function naturally, unless it is a closure stored in a variable.

The other minor issue is that any new functions/changes need to be kept in sync and tested

This is true, but this is also the case for curried functions, as you can see in the library you mentioned, it comes with both curried and uncurried functions as well as constants so you can reference those functions.

One thing PHP has that the other languages don't is that it allows objects to be iterable. So we have no need to distinguish between intermediate stuff and final stuff.

SamMousa avatar Mar 26 '19 15:03 SamMousa

To extend the example assuming you don't have a clean reference to your function:

$fluent->map(...)
    ->via(function(Fluent $fluent): iterable {
        return chunkBy($fluent);
    })
    ->slice(1, 6);

This ticks all my boxes:

  • [X] Readable
  • [X] Full IDE assistance since we don't use strings to refer to functions.
  • [X] Use third party functions even if they are not in the library.
  • [X] Doesn't use a generator so preserves rewindability.

I understand the arguments given against this, but PHP is not a functional programming language. It has drawbacks but also advantages. The advantage here is iterable, and that's what I'm trying to exploit to create this syntax that is in my opinion more readable than the alternatives.

SamMousa avatar Mar 26 '19 15:03 SamMousa

In the lib I mentioned, the curried functions are automatically generated. So it's an automated process that uses php parser to generate the code. Which is something you could do with a fluent interface as well I supposed.

Regarding that via example, that's not so bad, if you're functions were curried, then it should work fine:

<?php

namespace Acme\Util;

const chunkBy = chunkBy::class;
function chunk($args) {
  return function($value) use ($args) {};
}
use Acme\Util;

$fluent->map()->via(Util\chunk(args));

ragboyjr avatar Mar 26 '19 15:03 ragboyjr

https://github.com/krakphp/fn/blob/master/Makefile#L5 https://github.com/krakphp/fn/blob/master/make-curried.php

ragboyjr avatar Mar 26 '19 15:03 ragboyjr

In the lib I mentioned, the curried functions are automatically generated.

We could even omit the functions and just use __call(), and then document them via PHPDoc. For me both are viable, also adding new functions to this library doesn't happen that often so even duplicating is not that much of an effort.

SamMousa avatar Mar 26 '19 15:03 SamMousa

@SamMousa ya, I think that's what moneyphp does - https://github.com/moneyphp/money/blob/master/resources/generate-money-factory.php

ragboyjr avatar Mar 26 '19 15:03 ragboyjr

Yeah, they do it for static constructors. Anyway, I'm open to any solution for that problem, but still we need to decide if this fluent interface is something we want as an addition to the current functional interface.

I think I've given counters to most arguments, are there more issues with this solution?

SamMousa avatar Mar 26 '19 15:03 SamMousa

@nikic do you have an opinion?

SamMousa avatar Apr 03 '19 12:04 SamMousa

@SamMousa @nikic what would it take to finish this?

theofidry avatar Mar 24 '23 15:03 theofidry