jumpifbelow/php-functional-array

Classes to make the use of sets functional

14.0.0 2022-06-11 15:48 UTC

README

Development stage

The project is in a fairly advanced development phase. The API should be stable from now, but it is still possible that huge changes would be made. As small as it could be, any breaking change will come in major release, whatever it is.

General note

This package comes with 3 interfaces and 3 classes implementing them. Purpose of each interfaces:

  • FluentArrayInterface: allowing to use native PHP function in an object-oriented way.
  • ExtendedArrayInterface: like FluentArrayInterface but with custom operations implemented. It should be the one you want to use if you seek for more functionalities. If you do prefer to ensure the native performance of PHP, avoiding that class would ensure you that the called methods are using native PHP functions.
  • FluentIteratorInterface: inspired by FluentArrayInterface, but for virtually infinite set. It has less functions as many would not make sense to provide, but it handles big set without exhausting memory. It will only execute altering methods once iterating through it, allowing to handle exactly where your code will be executed, rather than on-call. It would handle a lot better any stream or resource providing data.

How to install it?

Just run Composer:

composer require jumpifbelow/php-functional-array

How to use it?

Two types of array are made.

The first, JumpIfBelow\Arrays\FluentArrayInterface is made to work like a PHP array, with all functions builtin directly in methods. All of the calls are immutable and fluent, meaning you can chain it without altering the first reference. The only exceptions is when managing internal pointer, where it won't send a new object as it would break the iterator.

The second, JumpIfBelow\Arrays\ExtendedArrayInterface is made to be like JumpIfBelow\Arrays\FluentArrayInterface. However, as it lacks some common methods, it adds them to be easier to handle.

Of course, the package comes with implementations you can already use like this:

<?php

use JumpIfBelow\Arrays\FluentArray;

$a = FluentArray::from([
    5,
    'er',
    'ert',
    'loop',
    false,
    null,
]);

$newArray = $a
    ->filter(function ($x): bool {
        return is_string($x);
    })
    ->map(function (string $x): string {
        return strrev($x);
    })
    ->sort()
;

var_dump($newArray->toArray());

// those examples could be shortened because they are using defined functions
// note that the interface accepts any callable, which allow this writing

$newArray = $a
    ->filter('is_string')
    ->map('strrev')
    ->sort()
;

// you can quickly setup an array and directly operate with it too
$pairSquareSum = FluentArray
    ::from([1, 4, 6, 5, 9, 8, 0])
    ->filter(fn(int $x) => $x % 2 === 0)
    ->map(fn(int $x) => $x ** 2)
    ->reduce(fn(int $sum, int $x) => $sum + $x, 0)
;

Then, there is the iterable object FluentIterable.

It is made with the idea to handle infinite sets without exhausting all of the memory and ending up with an error. Therefore, the execution relies a lot more on the CPU, the rest depending on your code. As it will be executed on run-time, the items will go through operators one by one, instead of generating getting each step completed before going to another one. This is benefical as you will receive items one by one as they are processed, rather than the whole big result block. You could imagine reading and parsing a very large file, while only acting on certain line, without having to load the whole file in the memory, but only the parts you are interested in.

<?php

use JumpIfBelow\Arrays\FluentIterable;

// we are creating a sum of a column in a CSV file
$sum = FluentIterable
    ::from(function () {
        $f = fopen('myfile.csv', 'r');

        while (($line = fgetcsv($f)) !== false) {
            yield $line;
        }

        fclose($f);
    })
    ->map(fn (array $row) => $row[1])
    ->filter(fn (int $value) => $value >= 0)
    ->reduce(fn (int $sum, int $value) => $sum + $value, 0)
;
// all of the code, from file opening and operations will only be executed when reduce is started
// once reduce is done, it will close the file handler

Note that transforming a FluentIterator to another one is an operation handled by an OperatorInterface. So while using the map operation, you only call in fact the MapOperator internally. The APIs are public to allow using your very own operators if needed. If we retake the above example, we could write it this way:

<?php

use JumpIfBelow\Arrays\FluentIterable;
use JumpIfBelow\Arrays\IterableOperator\{
    FilterOperator,
    MapOperator,
};

$sum = FluentIterable
    ::from(function () {
        $f = fopen('myfile.csv', 'r');

        while (($line = fgetcsv($f)) !== false) {
            yield $line;
        }

        fclose($f);
    })
    ->apply(
        MapOperator::with(fn (array $row) => $row[1]),
        FilterOperator::with(fn (int $value) => $value >= 0),
    )
    ->reduce(fn (int $sum, int $value) => $sum + $value, 0)
;

It is also powerful enough not to generate everything at once. For example, this would exhaust the memory right from the range call:

<?php

use JumpIfBelow\Arrays\FluentArray;

$array = FluentArray
    ::range(1, 2 ** 32 - 1)
    ->filter(fn (int $v): bool => $v >= 10 ** 7 && $v < 10 ** 8)
    ->slice(5, 10)
;

Meanwhile, you can go with a far greater set, filtering it, getting only a part of it, without actually going in the whole set. In this example, we would go from 0 to 2 ** 64 (if you are on a 64-bits OS). Then, only filtering values between 10,000,000 and 100,000,000. Afterwards, getting the values starting after the 5th one, and keeping only 10 of them. The whole set will not be generated all at once, and will not go further than 10,000,014.

<?php

use JumpIfBelow\Arrays\FluentIterable;

$iterable = FluentIterable
    ::range(0, PHP_INT_MAX)
    ->filter(fn (int $v): bool => $v >= 10 ** 7 && $v < 10 ** 8)
    ->slice(5, 10)
;

Immutable and mutable methods

This is supposed to only use immutable reference, meaning calling any function that edit the content should return a new version of the array with the altered data. The old reference remains unmodified. There are exceptions to this behavior because of the way some functions are behaving. Here is a list of methods:

FluentArray

Mutable

  • pop
  • shift

Immutable

  • arsort
  • asort
  • changeKeyCase
  • chunk
  • column
  • combine
  • countValues
  • diff
  • diffAssoc
  • diffKey
  • diffUassoc
  • diffUkey
  • fillKeys
  • filter
  • flip
  • intersect
  • intersectAssoc
  • intersectKey
  • intersectUassoc
  • intersectUkey
  • keys
  • krsort
  • ksort
  • map
  • merge
  • mergeRecursive
  • multisort
  • natcasesort
  • natsort
  • pad
  • push
  • rand
  • replace
  • replaceRecursive
  • reverse
  • rsort
  • shuffle
  • slice
  • sort
  • splice
  • uasort
  • udiff
  • udiffAssoc
  • udiffUassoc
  • uintersect
  • uintersectAssoc
  • uintersectUassoc
  • uksort
  • unique
  • unshift
  • usort
  • values
  • walk
  • walkRecursive

ExtendedArray

Mutable

  • nextKey
  • nextValue
  • nextEntry

Immutable

  • forEach
  • every
  • some
  • entry
  • findKey
  • findValue
  • quantiles
  • quantileKeys
  • quantileValues
  • flat
  • groupBy
  • indexBy
  • fetch
  • swap
  • with
  • fullMap
  • fullReduce
  • permutation
  • first
  • last

FluentIterable

Mutable

None

Immutable

  • apply
  • forEach
  • map
  • filter
  • reduce
  • replace
  • slice
  • unique
  • indexBy
  • some
  • every
  • toArray