Functional PHP
, (*1)
This library provides tools to write more functional PHP code. Its concise and consistent API
makes you more productive in different ways:
- Universal tools for processing iterables like arrays.
- Callback generation and modification functions.
- Optional value types to aid with stronger typing and erorr handling.
- Shorthand exception handling., (*2)
Table of contents
- Installation
- About
-
Usage
- Iterators
- Operations
- Exception handling
- Predicates
- The
Option type
- The
Result type
- Partial function application
- Currying
- Contributing
- Development
About
This library was written to address several concerns:
- Provide a single, consistent API to the different iterable
types in PHP, and the different operations available to the individual types: one API, any iterable,
always associative, access to keys.
- Provide iterable processing operations that do not yet exist in PHP.
- Make writing closures quick and easy. Predicate factories can be used to generate common (filter)
conditions.
- Allow developers to create functions that easily distinguish between different function outputs using
optional value types. These can be used to solve problems like with json_decode(), which returns
NULL in case of an error, or when it successfully decodes the JSON string null. It is impossible to distinguish
between the different outcomes without additional code, such as option types.
- Use native PHP features where possible for improved interoperability and performance. Naming and parameter order
follow the predominant conventions in PHP. This means all iterators implement \Iterator, and many PHP core functions
are used internally.
- Add laziness where possible, so many operations are only applied to the iterator items you actually
use., (*3)
Installation
Run composer require bartfeenstra/fu in your project's root directory., (*4)
Usage
To use any of the code, you must first import the namespaces at the top of your files:, (*5)
<?php
use BartFeenstra\Functional as F;
use BartFeenstra\Functional\Iterable as I;
use BartFeenstra\Functional\Predicate as P;
use function BartFeenstra\Functional\Iterable\iter;
?>
Iterators
Traversable/iterable data structures can be converted to universal iterators:, (*6)
Operations
The following operations work with iterator values, and even keys in the case of user-supplied callbacks:, (*7)
each
Executes code for every value., (*8)
<?php
$carrier = [];
$list = [3, 1, 4];
iter($list)->each(function (int $i) use (&$carrier) {
$carrier[] = $i;
});
assert($list === $carrier);
?>
filter
Filters out values that do not match., (*9)
<?php
$result = iter([3, 1, 4])->filter(P\gt(2));
assert([0 => 3, 2 => 4] === $result->toArray());
?>
find
Tries to find a single matching value., (*10)
<?php
$found = iter([3, 1, 4, 1, 5, 9])->find(P\gt(4));
assert(new I\SomeItem(5, 4) == $found);
?>
map
Converts all values individually., (*11)
<?php
$original = [3, 1, 4];
$expected = [9, 3, 12];
$result = iter($original)->map(function (int $i): int {
return 3 * $i;
});
assert($expected === $result->toArray());
?>
mapKeys
Converts all keys individually., (*12)
<?php
$original = [
3 => 'c',
1 => 'a',
4 => 'd',
];
$expected = [
9 => 'c',
3 => 'a',
12 => 'd',
];
$result = iter($original)->mapKeys(function (string $value, int $key): int {
return 3 * $key;
});
assert($expected === $result->toArray());
?>
reduce
Combines all values into a single one., (*13)
<?php
$list = [3, 1, 4];
$sum = iter($list)->reduce(function (int $sum, int $item): int {
return $sum + $item;
});
assert(new F\SomeValue(8) == $sum);
?>
To terminate the reduction before all items have been processed, throw a TerminateReduction with the final carrier
value., (*14)
fold
Combines all values into a single one, with a default start value., (*15)
<?php
$start = 2;
$list = [3, 1, 4];
$total = iter($list)->fold(function (int $total, int $item): int {
return $total + $item;
}, $start);
assert(10 === $total);
?>
To terminate the fold before all items have been processed, throw a TerminateFold with the final carrier value., (*16)
take
Takes n values., (*17)
<?php
$start = 2;
$list = [3, 1, 4, 1, 5, 9];
$result = iter($list)->take(4);
assert([3, 1, 4, 1] === $result->toArray());
?>
takeWhile
Take as many consecutively matching values as possible from the beginning., (*18)
<?php
$start = 2;
$list = [3, 1, 4, 1, 5, 9];
$result = iter($list)->takeWhile(P\le(3));
assert([3, 1] === $result->toArray());
?>
slice
Slices the values into a smaller collection., (*19)
<?php
$start = 2;
$list = [3, 1, 4, 1, 5, 9];
$result = iter($list)->slice(2, 3);
assert([2 => 4, 3 => 1, 4 => 5] === $result->toArray());
?>
min
Gets the lowest value., (*20)
<?php
$list = [3, 1, 4, 1, 5, 9];
$min = iter($list)->min();
assert(new F\SomeValue(1) == $min);
?>
max
Gets the highest value., (*21)
<?php
$list = [3, 1, 4, 1, 5, 9];
$max = iter($list)->max();
assert(new F\SomeValue(9) == $max);
?>
sum
Sums all values., (*22)
<?php
$list = [3, 1, 4, 1, 5, 9];
$sum = iter($list)->sum();
assert(new F\SomeValue(23) == $sum);
?>
forever
Infinitely repeats the set of values., (*23)
<?php
$list = [3, 1, 4];
$iterator = iter($list)->forever();
$expected = [3, 1, 4, 3, 1, 4, 3];
assert($expected === iterator_to_array($iterator->take(7), false));
?>
zip
Combines the values of two or more iterables into tuples., (*24)
<?php
$one = [3, 1, 4];
$two = [1, 5, 9];
$three = [2, 9, 2];
$zip = iter($one)->zip($two, $three);
$expected = [[3, 1, 2], [1, 5, 9], [4, 9, 2]];
assert($expected === iterator_to_array($zip));
?>
list
Converts all keys to integers, starting from 0., (*25)
<?php
$array = [
'a' => 'A',
'b' => 'B',
'c' => 'C',
];
$indexed = iter($array)->list();
$expected = ['A', 'B', 'C'];
assert($expected === iterator_to_array($indexed));
?>
listKeys
Uses keys as values, and indexes them from 0., (*26)
<?php
$array = [
'a' => 'A',
'b' => 'B',
'c' => 'C',
];
$keys = iter($array)->listKeys();
$expected = ['a', 'b', 'c'];
assert($expected === iterator_to_array($keys));
?>
flip
Swaps keys and values, similarly to array_flip()., (*27)
<?php
$array = [
'a' => 3,
'b' => 1,
'c' => 4,
];
$flipped = iter($array)->flip();
$expected = [
3 => 'a',
1 => 'b',
4 => 'c',
];
assert($expected === $flipped->toArray());
?>
reverse
Reverses the order of the values., (*28)
<?php
$array = [3, 1, 4];
$reverse = iter($array)->reverse();
assert([4, 1, 3] === $reverse->toArray());
?>
first
Gets the first value., (*29)
<?php
$array = [3, 1, 4, 1, 5, 9];
assert(new I\SomeItem(3, 0) == iter($array)->first());
?>
last
Gets the last value., (*30)
<?php
$array = [3, 1, 4, 1, 5, 9];
assert(new I\SomeItem(9, 5) == iter($array)->last());
?>
empty
Checks if there are no values., (*31)
empty());
assert(FALSE === iter([3, 1, 4])->empty());
?>
sort
Sorts items by their values., (*32)
<?php
$array = [
3 => 'c',
1 => 'a',
4 => 'd',
];
// ::sort() also takes an optional custom comparison callable.
$sort = iter($array)->sort();
$expected = [
1 => 'a',
3 => 'c',
4 => 'd',
];
assert($expected === iterator_to_array($sort));
?>
sortKeys
Sorts items by their keys., (*33)
<?php
$array = [
'c' => 3,
'a' => 1,
'd' => 4,
];
// ::sortKeys() also takes an optional custom comparison callable.
$sort = iter($array)->sortKeys();
$expected = [
'a' => 1,
'c' => 3,
'd' => 4,
];
assert($expected === iterator_to_array($sort));
?>
chain
Chains other iterables to an existing iterator, and re-indexes the values., (*34)
<?php
$arrayOne = [3, 1, 4];
$arrayTwo = [1, 5, 9];
$arrayThree = [2, 6, 5];
$iterator = iter($arrayOne)->chain($arrayTwo, $arrayThree);
$expected = [3, 1, 4, 1, 5, 9, 2, 6, 5];
assert($expected === $iterator->toArray());
?>
flatten
Flattens the iterables contained by an iterator into a single new iterator., (*35)
<?php
$array = [
[3, 1, 4],
[1, 5, 9],
[2, 6, 5],
];
$iterator = iter($array)->flatten();
$expected = [3, 1, 4, 1, 5, 9, 2, 6, 5];
assert($expected === $iterator->toArray());
?>
unique
Removes all duplicate values., (*36)
<?php
$objectOne = new \stdClass();
$objectTwo = new \stdClass();
$array = [0, false, false, null, [], [], '0', $objectOne, $objectOne, $objectTwo];
$iterator = iter($array)->unique();
$expected = [
0 => 0,
1 => false,
3 => null,
4 => [],
6 => '0',
7 => $objectOne,
9 => $objectTwo,
];
assert($expected === $iterator->toArray());
?>
Exception handling
Complex try/catch blocks can be replaced and converted to Result easily:, (*37)
Predicates
Predicates can be used with filter() and find(). They can be any
callable that takes a
single parameter and returns a boolean, but we added some shortcuts for common
conditions. These functions take configuration parameters, and return
predicates., (*38)
The Option type
In PHP, NULL signifies the absence of a value, but it is also used as a value itself. In such cases, an Option type
helps to distinguish between NULL as a value, and no value at all., (*39)
<?php
use BartFeenstra\Functional\Option;
use BartFeenstra\Functional\Some;
use BartFeenstra\Functional\SomeValue;
use BartFeenstra\Functional\None;
function get_option(): Option {
if (true) {
return new SomeValue(666);
}
return new None();
}
function handle_option(Option $value) {
if ($value instanceof Some) {
print sprintf('The value is %s.', $value());
}
// $value is an instance of None.
else {
print 'No value could be retrieved.';
}
}
handle_option(get_option());
?>
The Result type
The Result type can be used to complement or replace exceptions. As such, it is returned by
functions like try_except(). It represents success and a value, or an error., (*40)
<?php
use BartFeenstra\Functional\Ok;
use BartFeenstra\Functional\OkValue;
use BartFeenstra\Functional\Result;
use BartFeenstra\Functional\ThrowableError;
function get_result(): Result {
try {
// Do some things that may throw a ResultComputationException.
return new OkValue(666);
}
catch (\ResultComputationException $e) {
return new ThrowableError($e);
}
}
function handle_result(Result $result) {
if ($result instanceof Ok) {
print sprintf('The value is %s.', $result());
}
// $value is an instance of Error.
else {
print sprintf('An error occurred: %s.', $result);
}
}
handle_result(get_result());
?>
Partial function application
Partial function application is the creation of a new function with
zero or more parameters, based on an existing function, by fixing one or more of the arguments of the original function,
before calling it. Practically speaking, it allows you to copy a function, and fill out some of the arguments before
calling it. You can use this to quickly transform existing functions into anonymous functions that can be used as
callbacks. In PHP, this is possible with any kind of callable
(functions, methods, closures, ...)., (*41)
Currying
Currying converts a single function with n parameters to n functions with
one parameter each. Practically speaking, it allows you to copy a function, and fill out some of the arguments one at a
time before calling it. You can use this to quickly transform existing functions into anonymous functions that can be
used as callbacks. In PHP, this is possible with any kind of
callable (functions, methods, closures, ...)., (*42)
Contributing
Your involvement is more than welcome. Please
leave feedback in an issue,
or submit code improvements
through pull requests., (*43)
The internet, and this project, is a place for all. We will keep it friendly
and productive, as documented in our Code of Conduct,
which also includes the project maintainers' contact details in case you want
to report a situation, on behalf of yourself or others., (*44)
Development
Building the code
Run ./bin/build., (*45)
Testing the code
Run ./bin/test., (*46)
Fixing the code
Run ./bin/fix to fix what can be fixed automatically., (*47)
Code style
All PHP code follows PSR-2., (*48)