Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Operations: Compare, Max, Min #255

Merged
merged 3 commits into from
Jun 23, 2022
Merged

New Operations: Compare, Max, Min #255

merged 3 commits into from
Jun 23, 2022

Conversation

AlexandruGG
Copy link
Collaborator

@AlexandruGG AlexandruGG commented Jun 12, 2022

This PR:

  • Introduces two operations to compute the minimum and maximum value of a collection, as well as to compare items with a custom comparator
  • Is covered by unit tests
  • Has static analysis tests (psalm, phpstan)
  • Has documentation

Comment on lines +19 to +21
$result = Collection::fromIterable([1, 4, 3, 0, 2])
->min() // [4 => 0]
->current(); // 0
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to do it in this PR because I wanted it to be simple and just rely on foldLeft1, but I think we should make a dedicated PR before the next major release to change the behaviour of all reduction operations like reduce, foldLeft, foldRight etc.

While in some cases it might be useful to have these operations return a collection, it's much more common for users to expect these to return a single value; after all, that's the typical behaviour of reductions. This is the case in other languages (just a couple examples):

This change would make Collection easier to use and also make it easier to differentiate between some operations, like Reduce (will return a single value) and Reduction (will continue to return a collection).

WDYT @drupol?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do that before the next major release indeed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to work on this, we should gather our idea for next major versions, can't wait for it :=)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep! I'll create an issue and we can list out the methods we think need this treatment

@AlexandruGG AlexandruGG marked this pull request as ready for review June 12, 2022 19:05
@AlexandruGG AlexandruGG requested a review from drupol as a code owner June 12, 2022 19:05
Copy link
Collaborator

@drupol drupol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have another idea for this which would require a bit of rewriting if you're ok with this.

How about changing the behavior of the optional callback that we can pass to min() or max() ?

The callback would return an numeric value only and then, we compare for min and max internally in the operation, so we don't let the user alter the behavior of these operation through a callback.

Example:

$callback = static fn (object $o): int => $o->age;

$result = Collection::fromIterable([(object) ['id' => 2, 'age' => 5], (object) ['id' => 1, 'age' => 10]])
    ->min($callback)
    ->current(); // (object) ['id' => 2, 'age' => 5]

WDYT ?

docs/pages/code/operations/max.php Outdated Show resolved Hide resolved
docs/pages/code/operations/min.php Outdated Show resolved Hide resolved
@AlexandruGG
Copy link
Collaborator Author

I have another idea for this which would require a bit of rewriting if you're ok with this.

How about changing the behavior of the optional callback that we can pass to min() or max() ?

The callback would return an numeric value only and then, we compare for min and max internally in the operation, so we don't let the user alter the behavior of these operation through a callback.

Example:

$callback = static fn (object $o): int => $o->age;

$result = Collection::fromIterable([(object) ['id' => 2, 'age' => 5], (object) ['id' => 1, 'age' => 10]])
    ->min($callback)
    ->current(); // (object) ['id' => 2, 'age' => 5]

WDYT ?

Hey @drupol, thanks for looking at this.

I like the intent of restricting a bit the callback that can be passed to these operations. However, returning a single value from the callback you propose would not be sufficient because of two reasons I think:

  1. I might want to make the comparison on more than one value. This is quite common for objects and unfortunately in PHP we don't have something like the Comparable interface in Java. I can see there was an RFC at some point but wasn't implemented.

Because of this, I might want to do:

$maxCallback = static fn (stdClass $left, stdClass $right): stdClass => $left->age > $right->age && $left->height > $right->height
    ? $left
    : $right
;
  1. I might not want to use the max or min functions at all

I think the restriction at the type level should be sufficient, and further than that it can be the responsibility of the user to ensure the correct element is returned from the callback based on their desired effect.

I'm open to other suggestions though!

@drupol
Copy link
Collaborator

drupol commented Jun 16, 2022

Ok I understand and it's fine for me. However, I think we can already achieve this using a combination of the Sort and Head operations, no ?

@AlexandruGG
Copy link
Collaborator Author

Ok I understand and it's fine for me. However, I think we can already achieve this using a combination of the Sort and Head operations, no ?

Sure, I think that would work. But are you saying that these operations should be based on those ones instead of the fold, or that there isn't a need for min and max to exist?

@drupol
Copy link
Collaborator

drupol commented Jun 17, 2022

Hi Alex,

min and max would definitely be interesting to have. Especially that the implementation you made is mostly lazy.

FYI, the implementation of the Sort operation is not lazy and requires the iterator to be dumped into an ArrayIterator before calling the sort method(it's very ugly yeah !). This is something I would like to refactor to a more general operation "compare" that would be based on basically what you did in min and max.

Maybe this is something within this PR, adding a compare operation, and then use some kind of new operations like min and max using this new compare operation ? This way we can also get rid of code duplication and have 3 new operations !

WDYT ?

@AlexandruGG
Copy link
Collaborator Author

Hi Alex,

min and max would definitely be interesting to have. Especially that the implementation you made is mostly lazy.

FYI, the implementation of the Sort operation is not lazy and requires the iterator to be dumped into an ArrayIterator before calling the sort method(it's very ugly yeah !). This is something I would like to refactor to a more general operation "compare" that would be based on basically what you did in min and max.

Maybe this is something within this PR, adding a compare operation, and then use some kind of new operations like min and max using this new compare operation ? This way we can also get rid of code duplication and have 3 new operations !

WDYT ?

Hey @drupol,

Thanks for the clarifications.

It's not entirely clear to me how Sort would be refactored, so I would leave that to you, but regarding the other stuff are you thinking something like this?

$col = Collection::fromIterable([1, 2, 3]);

$callback = static fn ($left, $right) => $left > $right ? $right : $left;
$result = $col->compare($callback)->current(); // 3

$max = $col->max(); // internally uses `compare` + "max callback" -> 3
$min = $col->min(); // internally uses `compare` + "min callback" -> 1

If this is what you were thinking, should compare have a "default callback"? I'm not sure if one would be appropriate, so we could make the callback required for that operation - but at least min and max will be more standard and non-customizable.

@AlexandruGG AlexandruGG marked this pull request as draft June 20, 2022 08:08
@drupol
Copy link
Collaborator

drupol commented Jun 20, 2022

No worries for the sort operation, I'll deal with it in time.

For the rest, yes that's precisely that. WDYT about the idea? Would you be ok with that?

We could then even think to do that:

$obj1 = new StdClass;
$obj2 = new StdClass;
$obj1->weight = 1;
$obj2->weight = 2;
$col = Collection::fromIterable([$obj1, $obj2]);

$accessor = static fn (object $left): int => $left->weight; // Naming might be improved.
$callback = static fn ($left, $right) => $left > $right ? $right : $left;
$result = $col->compare($callback, $accessor)->current(); // 3

$max = $col->max($accessor); // internally uses `compare` + "max callback" -> 3
$min = $col->min($accessor); // internally uses `compare` + "min callback" -> 1

@AlexandruGG
Copy link
Collaborator Author

No worries for the sort operation, I'll deal with it in time.

For the rest, yes that's precisely that. WDYT about the idea? Would you be ok with that?

We could then even think to do that:

$obj1 = new StdClass;
$obj2 = new StdClass;
$obj1->weight = 1;
$obj2->weight = 2;
$col = Collection::fromIterable([$obj1, $obj2]);

$accessor = static fn (object $left): int => $left->weight; // Naming might be improved.
$callback = static fn ($left, $right) => $left > $right ? $right : $left;
$result = $col->compare($callback, $accessor)->current(); // 3

$max = $col->max($accessor); // internally uses `compare` + "max callback" -> 3
$min = $col->min($accessor); // internally uses `compare` + "min callback" -> 1

Yes, that looks good.

I'll work to implement those in this PR once I get the chance.

@AlexandruGG
Copy link
Collaborator Author

No worries for the sort operation, I'll deal with it in time.

For the rest, yes that's precisely that. WDYT about the idea? Would you be ok with that?

We could then even think to do that:

$obj1 = new StdClass;
$obj2 = new StdClass;
$obj1->weight = 1;
$obj2->weight = 2;
$col = Collection::fromIterable([$obj1, $obj2]);

$accessor = static fn (object $left): int => $left->weight; // Naming might be improved.
$callback = static fn ($left, $right) => $left > $right ? $right : $left;
$result = $col->compare($callback, $accessor)->current(); // 3

$max = $col->max($accessor); // internally uses `compare` + "max callback" -> 3
$min = $col->min($accessor); // internally uses `compare` + "min callback" -> 1

I think I didn't look at this properly the first time - it seems that in this scenario these operations are doing a hidden "map" and changing the collection from a list of stdClass to int via the accessor. I'm not in favour of this approach, it makes this more complex and harder to understand.

I'll implement a simpler version like the one proposed here.

@drupol
Copy link
Collaborator

drupol commented Jun 22, 2022

I think I didn't look at this properly the first time - it seems that in this scenario these operations are doing a hidden "map" and changing the collection from a list of stdClass to int via the accessor. I'm not in favour of this approach, it makes this more complex and harder to understand.

That's precisely that! What is wrong with that?

I'll implement a simpler version like the one proposed here.

As you wish :)

@AlexandruGG
Copy link
Collaborator Author

I think I didn't look at this properly the first time - it seems that in this scenario these operations are doing a hidden "map" and changing the collection from a list of stdClass to int via the accessor. I'm not in favour of this approach, it makes this more complex and harder to understand.

That's precisely that! What is wrong with that?

I think it makes the API and the typing more complex...it extends the range of things these comparison operations do, whereas it would be nicer to have a separation of responsibilities. I wouldn't expect a comparison operation to also change the type.

In any case the additions would be backwards compatible so we can always extend them if there's demand.

@drupol
Copy link
Collaborator

drupol commented Jun 22, 2022

I think it makes the API and the typing more complex...it extends the range of things these comparison operations do, whereas it would be nicer to have a separation of responsibilities. I wouldn't expect a comparison operation to also change the type.

It doesn't change it in the collection. It changes it locally so it can run the callback, but the original collection items stays unchanged.

Don't forget that most of the operations are lazy, so, applying a map or doing a local transformation is less or more the same in the end.

+ simplify `max` & `min`
@AlexandruGG AlexandruGG changed the title Introduce Max + Min Operations New Operations: Compare, Max, Min Jun 22, 2022
@AlexandruGG
Copy link
Collaborator Author

I think it makes the API and the typing more complex...it extends the range of things these comparison operations do, whereas it would be nicer to have a separation of responsibilities. I wouldn't expect a comparison operation to also change the type.

It doesn't change it in the collection. It changes it locally so it can run the callback, but the original collection items stays unchanged.

Don't forget that most of the operations are lazy, so, applying a map or doing a local transformation is less or more the same in the end.

I know it doesn't change the original collection and that the operations are lazy, that's not the issue. My concern was that the added complexity is not worth it in this case. Each operation can have it's own role - in this case compare effectively only selects from the original collection, doesn't need to do any further transformation.

@drupol
Copy link
Collaborator

drupol commented Jun 22, 2022

I know it doesn't change the original collection and that the operations are lazy, that's not the issue. My concern was that the added complexity is not worth it in this case. Each operation can have it's own role - in this case compare effectively only selects from the original collection, doesn't need to do any further transformation.

I fully agree !

@AlexandruGG AlexandruGG marked this pull request as ready for review June 22, 2022 08:29
@AlexandruGG
Copy link
Collaborator Author

This should be ready for review again, let me know if you're happy with this version! I think it's a nice addition for now, we can tweak things further later if needed :)

@drupol
Copy link
Collaborator

drupol commented Jun 22, 2022

This is indeed a very nice addition as usual :) I will carefully review this in the evening and let you know asap.

Thanks !!!

@drupol
Copy link
Collaborator

drupol commented Jun 22, 2022

Nothing to add here ! all good :) Ready to merge !

Copy link
Collaborator

@drupol drupol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@AlexandruGG
Copy link
Collaborator Author

Thank you! 😁

@AlexandruGG AlexandruGG merged commit 6cdf7fb into master Jun 23, 2022
@AlexandruGG AlexandruGG deleted the min-max-operations branch June 23, 2022 06:53
@drupol
Copy link
Collaborator

drupol commented Jun 23, 2022

Let's create issues for the remaining tasks we discussed in here !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants