Skip to content
This repository has been archived by the owner on Oct 1, 2019. It is now read-only.

Fluent arrays #1

Closed
wants to merge 6 commits into from
Closed

Fluent arrays #1

wants to merge 6 commits into from

Conversation

stancl
Copy link

@stancl stancl commented Sep 16, 2019

  • Feature Name: fluent-arrays
  • Start Date: 2019-09-16

Summary

Add a fluent API for working with arrays.

Motivation

PHP's native methods for working with arrays functionally (array_map, array_filter, array_reduce) are designed in a way that's consistent with C, rather than a way that's comfortable to write and read.

For example, array_filter takes the array as the first argument and the callback as the second one, while array_map takes the callback as the first argument and the array as the second one. This inconsistency makes understanding code hard.

Also, using these methods in a fluent way is much more natural than e.g. mapping the result of filter, because you get to read the operations in the same order as they happen. First you filter, then you map.

Reference-level explanation

-> calls to arrays should be converted to calls to the respective low-level functions:

  • The following code:
    [1, 2, 3]->map(function ($n) {
        return $n*2;
    });
    should be translated to:
    array_map(function ($n) {
        return $n*2;
    }, [1, 2, 3]);
  • The following code:
    $odd = [1, 2, 3]->filter(function ($n) {
        return $n & 1;
    });
    should be translated to:
    array_filter([1, 2, 3], function ($n) {
        return $n*2;
    });
  • The following code:
    $prices = $products->filter(function ($product) {
        return $product->category == 'foo';
    })->map(function ($product) {
        return $product->price;
    });
    should be translated to:
    $prices = array_map(function ($product) {
        return $product->price;
    }, array_filter($products, function ($product) {
        return $product->category == 'foo';
    }));

An alternative implementation would be to wrap -> calls to arrays in some global helper that would create an instance of a class like this:

class PHPPlusArr
{
    /** @var array */
    public $array;

    public function __construct(array $array)
    {
       $this->array = $array;
    }

    public function toArray(): array
    {
        return $this->array;
    }

    public function map(callable $callback): self
    {
        return new static(array_map($callback, $this->array));
    }

    public function filter(callable $callback): self
    {
        return new static(array_filter($this->array, $callback));
    }

    public function reduce(callable $callback, $initial = null): self
    {
        return new static(array_reduce($this->array, $callback, $initial));
    }
}

The benefit of this solution is that common methods like sum(), count(), etc can be made part of that class (as long as it doesn't grow to the size of Collection). An interesting solution, that would work for sum() and count() could be

public function __call($method, $args)
{
    return $method($this, ...$args);
}

Drawbacks

If the PHPPlusArr implementation is chosen, we would need to safely attach ->toArray() at the end of the chain.

Rationale and alternatives

An alternative to having this feature natively in the language/preprocessor is using something like Laravel Collections.

Unresolved questions

I'm not yet sure what's the best implementation for this. I'm also not sure how we can detect array variables correctly. Perhaps this should only work with variables that are strictly typed (something that could be provided by Plus) as array?

@nunomaduro nunomaduro added the rfc A new proposal label Sep 16, 2019
@nunomaduro nunomaduro changed the title RFC 0001: Fluent arrays Fluent arrays Sep 16, 2019
@ondrejmirtes
Copy link

I worry this is out of preprocessor scope because you’d have to recognize the type of expression ->map() is called on, in order to recognize if it’s an array or not.

@m1guelpf
Copy link

m1guelpf commented Sep 16, 2019

I implemented something similar to this on native PHP using the scalar objects extension a few months ago. If you check my code, it just used the __call or __callStatic methods to proxy the method call to the Laravel Collection class and then return the result. If we go with an approach like this (which I'd recommend because Collection is already a complete and tested class), you'd just need to modify my code to modify the array instead of just returning it. The downside would be adding another dependency to Plus.

A third approach would be to just implement the ability to define scalar objects using Pre (implementing the aforementioned extension on Plus) and let people add their own array implementations. I can open another RFC for this if you want.

@ondrejmirtes
Copy link

The reason why I think this isn't suitable for a preprocessor is because guessing type of variable in PHP is very hard and unreliable. I know what I'm talking about - I wrote PHPStan :)

Plus should stick to changing what's readable from AST, which leads to predictable behaviour.

Even static analysis cannot always save you, if mixed expressions or phpDocs are involved.

@antonioribeiro
Copy link

I think it could be extended to do what my package Coollection is able to do: access multidimensional collections (arrays) like we do with objects:

$countries->where('name.common', 'United States')->first()->currency->name->english;

Instead of

$vimeo['body']['data'][0]['metadata']['connections']['likes']['total'];

Do

$vimeo->body->data->first()->metadata->connections->likes->total;

@stancl
Copy link
Author

stancl commented Sep 17, 2019

@ondrejmirtes Detecting arrays is certainly a hard task, but what if this feature only worked with variables that have a fixed array type?

array $foo = [1, 2, 3];
$foo->map(); // works

$bar = [1, 2, 3];
$bar->map(); // doesn't work

@ondrejmirtes
Copy link

You still need to do some nontrivial type inference. Imagine that if branches or switch is involved.

@ondrejmirtes
Copy link

The problem is that if the type inference fails and the code isn't transformed, the repercussions are really bad - you're calling a method on an array in runtime.

@nunomaduro nunomaduro closed this Sep 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
rfc A new proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants