rfc:array_partition

PHP RFC: array_partition()

Introduction

Splitting an array into the elements that satisfy a condition and the elements that do not is a common operation. PHP has no built-in function for it, so code today does one of the following:

// Two passes, with the condition duplicated and negated
$adults = array_filter($users, fn($u) => $u->age >= 18);
$minors = array_filter($users, fn($u) => $u->age < 18);
 
// One filter plus a set difference on the keys
$adults = array_filter($users, fn($u) => $u->age >= 18);
$minors = array_diff_key($users, $adults);
 
// A hand-written loop
$adults = [];
$minors = [];
foreach ($users as $key => $user) {
    if ($user->age >= 18) {
        $adults[$key] = $user;
    } else {
        $minors[$key] = $user;
    }
}

The first form repeats the condition and calls the callback twice per element. The second relies on a less obvious idiom. The third is correct but verbose for a simple intent.

This RFC proposes a single function that states the intent directly:

[$adults, $minors] = array_partition($users, fn($u) => $u->age >= 18);

Proposal

Add the following function to ext/standard. Its behaviour is that of this userland implementation:

function array_partition(array $array, callable $callback): array {
    $matching = [];
    $nonMatching = [];
 
    foreach ($array as $key => $value) {
        if ($callback($value, $key)) {
            $matching[$key] = $value;
        } else {
            $nonMatching[$key] = $value;
        }
    }
 
    return [$matching, $nonMatching];
}

The only observable difference is that the native function keeps by-reference elements of the input as references, like array_filter() (see “By-reference elements” below and “Open Issues”).

The callback is called once for each element, in array order, and receives the value and the key, like array_find(), array_find_key(), array_any() and array_all(). The function returns a list of exactly two arrays:

  • index 0: the elements for which the callback returned a truthy value;
  • index 1: all other elements.

Keys are preserved in both arrays, and the relative order of the elements is kept. The input array is not modified.

Examples

var_dump(array_partition([1, 2, 3, 4, 5], fn($v) => $v % 2 === 1));
// [[0 => 1, 2 => 3, 4 => 5], [1 => 2, 3 => 4]]
 
[$named, $indexed] = array_partition(
    ['a' => 1, 'b' => 2, 5 => 3, 6 => 4],
    fn($value, $key) => is_string($key),
);
// $named   = ['a' => 1, 'b' => 2]
// $indexed = [5 => 3, 6 => 4]
 
var_dump(array_partition([], fn($v) => true));
// [[], []]

Semantics in detail

  • The return value of the callback is interpreted with the usual boolean conversion, as in array_filter().
  • array_partition($a, $cb)[0] is identical (===) to array_filter($a, $cb, ARRAY_FILTER_USE_BOTH), and array_partition($a, $cb)[1] is identical to array_diff_key($a, array_partition($a, $cb)[0]).
  • An empty input returns [[], []], so the result can always be destructured into two variables.
  • If the callback throws, the exception is propagated, no further elements are visited and no partial result is returned.
  • Elements that are references in the input stay references in the result, as with array_filter(). See “By-reference elements” below.
  • There is no $mode parameter. The callback always receives both the value and the key, following array_find() rather than array_filter(). A callback that accepts only the value works unchanged, because userland functions ignore extra arguments.

Like array_find(), passing an internal function with a fixed arity as the callback fails, because it receives two arguments:

array_partition(['x'], 'is_string');
// ArgumentCountError: is_string() expects exactly 1 argument, 2 given
 
array_partition(['x'], is_string(...));  // same error
array_partition(['x'], fn($v) => is_string($v));  // works
array_partition(['x'], is_string(?));  // works, partial function application (PHP 8.6+)

By-reference elements

An element that is a reference in the input, and is still referenced elsewhere, is a reference in the result too. Writing to it through the result changes the input, and the other way round:

$input = [1, 2, 3];
$ref = &$input[0];
 
[$matching, $rest] = array_partition($input, fn($v) => $v === 1);
 
$matching[0] = 'changed';
var_dump($input[0]); // string(7) "changed"
 
$ref = 'again';
var_dump($matching[0]); // string(5) "again"

A reference that is no longer shared, for example after unset($ref), is copied as a plain value:

$input = [1, 2, 3];
$ref = &$input[0];
unset($ref);
 
[$matching, $rest] = array_partition($input, fn($v) => $v === 1);
 
$matching[0] = 'changed';
var_dump($input[0]); // int(1)

This is the same behaviour as array_filter(), array_diff_key(), array_slice(), array_values() and array_chunk(). Whether array_partition() should keep it is listed under “Open Issues”.

Why the matching elements come first

Every prior-art implementation listed below returns the matching elements first. That order also reads naturally in the common destructuring form [$matching, $rest] = array_partition(...).

A result keyed by true and false was suggested during the discussion. PHP casts boolean array keys to integers (true becomes 1 and false becomes 0), so such a result would have to put the non-matching elements at index 0. Whichever layout is chosen, one of the two idioms silently returns the wrong bucket. This RFC keeps the order used by other languages. Note that, with the proposed layout, the following does not work as it reads:

// Wrong: true is cast to 1, so $passed receives the NON-matching elements
[true => $passed, false => $rejected] = array_partition($values, $callback);
 
// Correct
[$passed, $rejected] = array_partition($values, $callback);

Why not a general grouping function?

A function that groups elements by an arbitrary key returned by the callback is more general, and is already the subject of a separate RFC (see “Related RFCs”). It does not replace a two-way split:

  • The buckets are only created when at least one element falls into them, so destructuring needs a fallback ($groups[true] ?? []) for each side. Defaults in destructuring itself were proposed by the Destructuring Coalesce RFC, which was declined.
  • Boolean callback results would have to be used as keys, which leads to the casting problem described above.
  • Declaring the groups up front, for example with an extra parameter listing the keys to create, solves the missing buckets but not the casting problem. List destructuring matches by key, not by position, so seeding [true, false] still produces the keys 1 and 0, and [$matching, $rest] = ... receives the buckets swapped. String labels avoid this (fn($u) => $u->age >= 18 ? 'adults' : 'minors' with ['adults', 'minors']). However, the callback, the list of keys and the destructuring keys then have to agree, and a typo silently creates an extra group.
  • The languages listed below that have both concepts provide them as separate functions (for example Kotlin partition and groupBy, Rust partition and the itertools group_by, Laravel partition and groupBy).

PHP already provides simpler, specialised functions alongside general ones, such as array_find() and array_any() alongside array_filter(). The two proposals are complementary, and this RFC does not block a future grouping function.

Parameter naming

The parameters are named $array and $callback, as for the other callback-taking array functions (array_filter(), array_map(), array_walk(), array_find(), array_find_key(), array_any(), array_all()). $predicate was suggested as a more descriptive name. It was not chosen, so that the callback: named argument works the same way across the whole family.

Prior art

  • Laravel: Illuminate\Support\Collection::partition() returns two collections, passing first. The callback receives the value and the key, and keys are preserved.
  • Ruby: Enumerable#partition returns [true_elements, false_elements].
  • Kotlin: Iterable<T>.partition() returns a Pair whose first holds the elements for which the predicate is true.
  • Rust: Iterator::partition() returns a tuple (matching, non_matching).
  • Haskell: Data.List.partition returns a pair (matching, non_matching).
  • JavaScript: an Array.prototype.partition proposal was published in 2020 (https://github.com/joaolucasl/proposal-array-prototype-partition, discussion at https://es.discourse.group/t/array-prototype-partition-like-filter-but-two-arrays-for-true-and-false/447) but did not advance. ECMAScript 2024 added the general Object.groupBy() and Map.groupBy() instead. Lodash provides _.partition().

Backward Incompatible Changes

Adding a new function is not considered a backward incompatible change, but the name array_partition becomes reserved in the global namespace. Code that declares its own global array_partition() without a function_exists() guard fails with “Cannot redeclare function array_partition()”. Code with such a guard no longer declares its own version and silently uses the native function instead, which may behave differently. In both cases the code needs to be adjusted: renamed, namespaced, or checked against the native semantics.

Existing userland functions with this name often use a different meaning: they split an array into N chunks.

  • The array-partition WordPress plugin by coffee2code defined a global array_partition($array, $number_of_columns) in version 1.0, guarded by function_exists() from 1.0.2. It was renamed to c2c_array_partition() in 1.1 and the deprecated name was removed in 1.3 (2020-09-10). Sites running a version older than 1.3 would no longer fatal on declaration because of the guard. However, calls such as array_partition($posts, 3) would reach the native function and throw a TypeError.
  • php-src issue #14582 (June 2024) requested an array_partition() that splits an array into a given number of parts, like array_chunk() but by count. It was closed by its author, who noted that array_chunk($array, (int) ceil(count($array) / $n)) covers the use case.

The name was kept because “partition” by predicate is the established meaning across the languages listed under “Prior art”, and because the N-chunks operation is already available through array_chunk(). The documentation should state the difference explicitly.

Proposed PHP Version(s)

Next minor version (PHP 8.7).

RFC Impact

To the Ecosystem

IDEs, language servers and static analysers need to add the new function to their stubs. A precise return type can be expressed as array{0: array<K, V>, 1: array<K, V>} for an input of type array<K, V>. Polyfills are straightforward, because the behaviour can be reproduced with array_filter() and array_diff_key(), or with a foreach loop.

To Existing Extensions

None. Only ext/standard gains a function. array_filter() and all other existing array functions are unchanged.

To SAPIs

None.

Performance

The value of this function is clarity, not speed: the cost is dominated by invoking the callback. Measured on a release build (./configure --disable-all, php -n), Apple M3 Pro under Docker (aarch64), callback fn($v) => $v % 2 === 0, median of 21 runs over 3 rounds, 1,000,000 elements:

Approach Packed int list String keys
array_partition() 40.2 - 42.2 ms 42.1 - 45.3 ms
array_filter() + array_diff_key() 39.8 - 40.4 ms 42.8 - 44.6 ms
array_filter() twice 88.5 ms 90.3 ms
foreach loop 34.8 - 35.7 ms 40.4 - 45.4 ms

array_partition() is on par with array_filter() + array_diff_key() (within about 2%), about 2.2 times faster than filtering twice, and about 5 - 12% slower than a hand-written loop, which avoids the function call per element. The benchmark script and the raw output are available on request.

Open Issues

  • Reference handling. The proposed implementation keeps references, as described in “By-reference elements”. This matches the array functions that return part of their input (array_filter(), array_diff_key(), array_slice(), array_values(), array_chunk()), and means that replacing the array_filter() + array_diff_key() idiom with array_partition() does not change behaviour. On the other hand, the behaviour cannot be reproduced exactly in userland, and writing to a result element can modify the input. array_find() drops references, but it returns a single value rather than an array. Dropping references instead would be a one-line change in the implementation. Feedback on which behaviour is preferred is welcome.

Future Scope

  • Support for iterable or generators (for example accepting a Traversable and returning two arrays). This RFC follows the other array_* functions and only accepts arrays.
  • A general grouping function, as proposed by the array_group RFC.

Voting Choices

Primary Vote requiring a 2/3 majority to accept the RFC:

Add array_partition() as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

Implementation

After the RFC is implemented, this section should contain:

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Rejected Features

  • A $mode parameter mirroring array_filter() (ARRAY_FILTER_USE_KEY / ARRAY_FILTER_USE_BOTH): removed after review in favour of the array_find() signature.
  • A result keyed by true / false: see “Why the matching elements come first”.
  • Naming the callback parameter $predicate: see “Parameter naming”.

Changelog

  • 0.1 (2026-10-25): Initial draft.
rfc/array_partition.txt · Last modified: by ayoubezzini