0

Functional array operations: array_map, array_filter, array_reduce

Intermediate5 min read·php-04-010
interviewcompare

Concept

array_map(), array_filter(), and array_reduce() are PHP's trinity of functional array operations, inspired by the same primitives in Lisp and Haskell. They enable composable, side-effect-free data transformations that are easier to test and reason about than equivalent imperative loops.

array_map(callable $callback, array ...$arrays) applies a callback to every element and returns a new array with the transformed values. The original array is not modified. When passed a single array, the keys of the result match the input keys for associative arrays but are reindexed (0, 1, 2...) for numeric arrays — this asymmetry surprises developers. When passed multiple arrays, keys are dropped and result is always numerically indexed; the callback receives corresponding elements from each array.

array_filter(array $array, ?callable $callback, int $mode) returns a new array containing only elements for which the callback returns a truthy value. If no callback is given, it filters out falsy values (false, 0, '', null, []). Critically, array_filter() preserves original keys. After filtering, you may have gaps in integer keys (e.g., [0 => 'a', 2 => 'c'] after removing index 1). If you need a contiguous 0-based result, call array_values() after array_filter(). The $mode parameter controls whether the callback receives the value (default, ARRAY_FILTER_USE_VAL), the key (ARRAY_FILTER_USE_KEY), or both key and value (ARRAY_FILTER_USE_BOTH).

array_reduce(array $array, callable $callback, mixed $initial) accumulates the array into a single value by repeatedly applying fn($carry, $item). The first call receives $initial as $carry. This is the most general of the three — both array_map and array_filter can be expressed as array_reduce, but the specialized functions are faster.

In PHP 8.4, all three functions accept first-class callables and arrow functions cleanly. They compose well with each other and with usort() to build readable data pipelines.

FunctionReturnsModifies originalPreserves keys
array_map()New arrayNoAssoc: yes; Numeric: yes (reindexed if multi-array)
array_filter()New arrayNoYes (always)
array_reduce()Scalar/array/anyNoN/A

Code Example

php
<?php
declare(strict_types=1);

// ---- array_map ----
$numbers = [1, 2, 3, 4, 5];
$squared = array_map(fn(int $n): int => $n ** 2, $numbers);
// [1, 4, 9, 16, 25]

// Key preservation: associative arrays keep keys
$prices = ['apple' => 1.20, 'banana' => 0.50, 'cherry' => 2.00];
$taxed = array_map(fn(float $p): float => round($p * 1.2, 2), $prices);
// ['apple' => 1.44, 'banana' => 0.60, 'cherry' => 2.40]

// Multi-array: keys dropped, parallel iteration
$a = [1, 2, 3];
$b = [10, 20, 30];
$sums = array_map(fn($x, $y) => $x + $y, $a, $b);
// [11, 22, 33]

// ---- array_filter ----
$values = [0, 1, '', 'hello', null, false, [], 42];
$truthy = array_filter($values); // removes falsy values
print_r($truthy); // [1=>1, 3=>'hello', 7=>42] — GAPS in keys!

$clean = array_values(array_filter($values)); // compact keys: [1,'hello',42]

// Filter with callback
$evens = array_filter($numbers, fn(int $n): bool => $n % 2 === 0);
// [1=>2, 3=>4]

// ARRAY_FILTER_USE_BOTH — callback receives value AND key
$data = ['user_id' => 42, 'admin' => true, '_internal' => 'secret'];
$public = array_filter(
    $data,
    fn($value, string $key): bool => !str_starts_with($key, '_'),
    ARRAY_FILTER_USE_BOTH,
);
// ['user_id' => 42, 'admin' => true]

// ---- array_reduce ----
$items = [
    ['name' => 'apple',  'qty' => 3, 'price' => 1.20],
    ['name' => 'banana', 'qty' => 5, 'price' => 0.50],
];
$total = array_reduce(
    $items,
    fn(float $carry, array $item): float => $carry + ($item['qty'] * $item['price']),
    0.0,
);
echo $total; // 6.10

// Building an array with reduce (group by first letter)
$words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry'];
$grouped = array_reduce($words, function (array $carry, string $word): array {
    $carry[$word[0]][] = $word;
    return $carry;
}, []);
// ['a' => ['apple','avocado'], 'b' => ['banana','blueberry'], 'c' => ['cherry']]

// ---- Composing: filter → map ----
$activeUsers = [
    ['name' => 'Alice', 'active' => true],
    ['name' => 'Bob',   'active' => false],
    ['name' => 'Carol', 'active' => true],
];
$names = array_map(
    fn(array $u): string => $u['name'],
    array_filter($activeUsers, fn(array $u): bool => $u['active']),
);
// ['Alice', 'Carol'] — note: numeric keys might not be 0,1 if filter was applied first
$names = array_values($names); // compact to [0=>'Alice', 1=>'Carol']

Interview Q&A

Q: array_map() on a single associative array preserves string keys, but on a numerically-indexed array it also preserves the numeric keys. Why then does passing two arrays to array_map() drop all keys?

When array_map() receives multiple array arguments, it must iterate them in parallel by position. To identify corresponding elements across arrays, it uses a positional (numeric) counter, regardless of what keys the individual arrays have. Since the callback receives elements from multiple arrays simultaneously, maintaining the original keys would be ambiguous — which array's keys take priority? PHP resolves this by always producing a fresh 0-indexed result array in multi-array mode. This behavior is documented but counterintuitive for developers who expect symmetry with single-array mode.


Q: A pipeline runs array_filter() followed by array_map(). The intermediate result has gaps in integer keys (e.g., [0, 2, 4]). Does array_map() care about the gaps?

No, array_map() iterates the array using PHP's internal bucket traversal, which follows insertion order and visits every bucket regardless of key gaps. It will correctly process elements at keys 0, 2, and 4, and the output will preserve those same keys (0, 2, 4) in the result. If you need the output to have compact keys (0, 1, 2), call array_values() either before array_map() or after. The common idiom for a clean pipeline is array_values(array_map(fn, array_filter($input))).


Q: How would you express array_map() using array_reduce(), and in what scenarios would you prefer array_reduce() over multiple separate map/filter passes?

array_map(fn($x) => f($x), $arr) is equivalent to array_reduce($arr, fn($carry, $x) => [...$carry, f($x)], []). However, the [...$carry, f($x)] spread inside reduce creates a new array on every iteration — O(n²) — so the array_map version is always faster for pure transformation. array_reduce() is preferred when you need to both transform and aggregate in a single pass — for example, simultaneously computing a sum and building an index array. Combining a filter, map, and aggregate into one array_reduce() eliminates two intermediate array allocations and is the right choice in memory-constrained environments or when processing data in a single traversal is semantically important.