Skip to content
academia.sh

Lesson 09 / 14

Array Functions

The filter, map and reduce functions; array_search returning false when it finds nothing; a match at index zero being confused with false in a loose comparison; array_combine with mismatched lengths and a negative str_repeat call throwing ValueError.

Published Updated

In this lesson

  • Filters, maps and reduces an array to a single value with array_filter, array_map and array_reduce
  • Measures that array_search returns false when it finds nothing and records it under the return channel
  • Shows that a match at index zero counts as not found in a loose comparison and that strict equality tells the two apart
Contents

In the Multidimensional Arrays lesson, the break of a path was heard as null and E_WARNING. With array functions the channel changes: when the sought element is not found, no level is reported; the function returns a special value, and interpreting that value becomes the job of the calling code. The lesson uses the channel names in the sense they had in the previous lessons: silent, return, warning, halts.

PHP’s array functions are a broad family, and the lesson does not walk through all of them. Two groups are measured: the trio of filter, map and reduce, which transforms an array, and the functions that search an array for an element. The first is the data flow itself, and the second is the plainest place to see where a problem is heard from.

Filter, Map and Reduce

The three functions ask three separate questions. Filter answers “which elements stay”, map answers “what does each element become”, and reduce answers “how do they all come down to a single value”. The file below runs all three over the same list of numbers.

<?php
// transform.php — filter, map and reduce
$numbers = [1, 2, 3, 4, 5, 6];

$evens = array_filter($numbers, fn($n) => $n % 2 === 0);
$squares = array_map(fn($n) => $n * $n, $numbers);
$total = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);

echo 'evens: ', json_encode($evens), "\n";
echo 'squares: ', json_encode($squares), "\n";
echo 'total: ', $total, "\n";
echo 'keys of evens: ', implode(',', array_keys($evens)), "\n";
echo 'renumbered: ', json_encode(array_values($evens)), "\n";
evens: {"1":2,"3":4,"5":6}
squares: [1,4,9,16,25,36]
total: 21
keys of evens: 1,3,5
renumbered: [2,4,6]

array_filter keeps the keys of the retained elements. Because the keys of the evens list are 1, 3, 5, json_encode writes it as an object and not as a list; array_values renumbers the keys. This is the same behavior as unset in the Indexed and Associative Arrays lesson: an array that looks indexed can stop being indexed after filtering. array_map, in turn, carries the keys along with the elements; when a single array is given, the result uses the same set of keys.

AS10. array_reduce binds the first parameter of the callback to an accumulator value, and the initial value is the third argument. If no initial value is given, the accumulator starts as null; when an empty array is reduced, the result in that case is null. The lesson always gives the initial value explicitly and does not separately measure the empty-array behavior.

An Element That Is Not Found

Functions that search for an element report a problem not with a warning but with a return value. array_search returns the key of the sought value if it finds it, and false if it does not. The listener classifies this behavior with the channel order; channel.php is given below in full, and since it has no output block, it is only written to disk.

<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
<?php
// search.php — an element not found and ValueError
require 'channel.php';

$observations = [
    'element not found' => fn() => array_search('apple', ['pear', 'quince']),
    'element found'     => fn() => array_search('quince', ['pear', 'quince']),
    'first element'     => fn() => array_search('pear', ['pear', 'quince']),
    'negative repeat'   => fn() => str_repeat('a', -1),
    'combine mismatch'  => fn() => array_combine(['a', 'b'], [1]),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-20s%-9s%s\n", $name, $channel, $evidence);
}
element not found   return   result false
element found       silent   result int 1
first element       silent   result int 0
negative repeat     halts    ValueError
combine mismatch    halts    ValueError

The first line is the course’s element not found observation and belongs to the return channel: array_search('apple', ['pear', 'quince']) returned false, no level was reported, and execution continued. The problem is heard only by code that looks at the return value. The second line shows a successful search: 1, the key of the element found, is a silent result. The third line prepares the source of the problem: when the first element is found, the key returned is 0.

The last two lines show another channel of the same family of functions. str_repeat('a', -1) and an array_combine call with two arrays of different lengths throw ValueError; the channel is halts. When the value of an argument has the right type but lies outside the acceptable range, the function does not quietly produce a wrong result; it stops. Two different channels exist in the same family of functions: “not found” is a return value, and “invalid call” is an exception.

AS11. array_combine and str_repeat are examples of the ValueError channel only. Which function gives ValueError or false under which condition is written in the documentation of each function, and the lesson does not list it; what is measured is that “not found” comes from the return channel and “invalid call” from the exception channel.

Sorting and Slicing

All sorting functions modify the array in place and treat keys differently: sort discards the keys and renumbers, asort sorts by value while keeping the keys, and ksort sorts by key. array_slice, in turn, renumbers integer keys by default.

<?php
// sorting.php — the effect of sorting and slicing on keys
$raw = [3 => 'c', 1 => 'a', 2 => 'b'];

$a = $raw;
sort($a);
echo 'sort: ', json_encode($a), "\n";

$b = $raw;
asort($b);
echo 'asort: ', json_encode($b), "\n";

$c = $raw;
ksort($c);
echo 'ksort: ', json_encode($c), "\n";

$d = $raw;
rsort($d);
echo 'rsort: ', json_encode($d), "\n";

echo 'slice: ', json_encode(array_slice($raw, 1)), "\n";
echo 'slice, keys preserved: ', json_encode(array_slice($raw, 1, null, true)), "\n";
echo 'sort return: ', var_export(sort($a), true), "\n";
sort: ["a","b","c"]
asort: {"1":"a","2":"b","3":"c"}
ksort: {"1":"a","2":"b","3":"c"}
rsort: ["c","b","a"]
slice: ["a","b"]
slice, keys preserved: {"1":"a","2":"b"}
sort return: true

sort and rsort discard the associative information; asort and ksort keep the key, and json_encode therefore writes them as objects. array_slice renumbers integer keys unless the preserve_keys argument is given. The last line shows a contract the sorting functions keep: they modify the array in place and return true; they are not functions that return a sorted copy. Code that writes $sorted = sort($array) sorts the array but assigns true to the variable $sorted.

AS12. The stability of the sorting functions (keeping the relative order of equal elements) is not measured in this lesson; the lesson tests only what happens to the keys and the return value. Sorting an array of mixed types depends on the comparison rules of the language, and no example is given here.

The Membership Query

in_array asks whether a value is present in the array and uses the same loose comparison as array_search. When true is given as the third argument, the comparison becomes strict.

<?php
// membership.php — in_array with loose and strict comparison
$numbers = ['10', '20'];

echo "loose, '1e1': ", var_export(in_array('1e1', $numbers), true), "\n";
echo "strict, '1e1': ", var_export(in_array('1e1', $numbers, true), true), "\n";
echo "loose, 10: ", var_export(in_array(10, $numbers), true), "\n";
echo "strict, 10: ", var_export(in_array(10, $numbers, true), true), "\n";
echo "loose null, [0]: ", var_export(in_array(null, [0]), true), "\n";
echo "strict null, [0]: ", var_export(in_array(null, [0], true), true), "\n";

$repeated = ['1', 1, 'a', 'A', 'a'];
echo 'unique: ', json_encode(array_unique($repeated)), "\n";
loose, '1e1': true
strict, '1e1': false
loose, 10: true
strict, 10: false
loose null, [0]: true
strict null, [0]: false
unique: {"0":"1","2":"a","3":"A"}

In a loose comparison, the string '1e1' counts as equal to '10', because both are numeric strings and their value is 10; the strict comparison separates the two. The same rule holds for the integer 10 and the string '10'. null and 0 are equal in a loose comparison and not in a strict one. These results are not a badly written condition; they are the rule of loose equality, and the coercion measured in the Programming Fundamentals course (M06/K01) affects the result of a search here. Because the output of in_array is only true or false, a false positive is heard through no channel: the channel is silent. On the last line, array_unique keeps the first value it sees and counts '1' and 1, whose string representation is the same, as one; 'a' and 'A' stay separate.

Silent Behaviors in Mapping and Summing

Mapping, flipping and summing functions often report no level when the shape of the input differs from what was expected. The observations below are classified with the same listener.

<?php
// quiet.php — input problems in mapping, flipping and summing
require 'channel.php';

$observations = [
    'two arrays, unequal'  => fn() => array_map(fn($a, $b) => [$a, $b], [1, 2, 3], ['x']),
    'duplicate value flip' => fn() => array_flip(['a', 'b', 'a']),
    'duplicate key'        => fn() => array_combine(['a', 'a'], [1, 2]),
    'numeric string sum'   => fn() => array_sum(['3', '4']),
    'non-numeric sum'      => fn() => array_sum([1, 'abc']),
    'empty array sum'      => fn() => array_sum([]),
    'empty array max'      => fn() => max([]),
    'key filter'           => fn() => array_filter(['a' => 1, 'b' => 2], fn($k) => $k === 'a', ARRAY_FILTER_USE_KEY),
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-23s%-9s%s\n", $name, $channel, $evidence);
}

echo 'flip: ', json_encode(array_flip(['a', 'b', 'a'])), "\n";
echo 'combine: ', json_encode(array_combine(['a', 'a'], [1, 2])), "\n";
echo 'map: ', json_encode(array_map(fn($a, $b) => [$a, $b], [1, 2, 3], ['x'])), "\n";
two arrays, unequal    silent   result array(3)
duplicate value flip   silent   result array(2)
duplicate key          silent   result array(1)
numeric string sum     silent   result int 7
non-numeric sum        warning  E_WARNING, result int 1
empty array sum        silent   result int 0
empty array max        halts    ValueError
key filter             silent   result array(1)
flip: {"a":2,"b":1}
combine: {"a":2}
map: [[1,"x"],[2,null],[3,null]]

When array_map receives two arrays, it pads the shorter one with null and reports no level; the pairs [2,null] and [3,null] are produced silently. array_flip and array_combine keep the last value on a duplicate value or key and silently discard the first: {"a":2,"b":1} and {"a":2}.

In summing, numeric strings are converted to numbers (the sum of '3' and '4' is 7), whereas a non-numeric element produces E_WARNING and the total counts only the valid elements. On an empty array, array_sum gives 0 and max halts with ValueError; on an array that may be empty, max must be guarded before the call with count or [] !== $array. ARRAY_FILTER_USE_KEY gives the key to the callback instead of the value and works silently.

The Bounding Measurement

The return channel works only if the calling code reads it correctly. The returns 0 and false of array_search are the same value in a loose equality. Two spellings interpret the same list in two different ways.

<?php
// distinction.php — 0 and false are the same value in a loose comparison
$items = ['pear', 'quince'];

$loose = array_search('pear', $items) == false ? 'not found' : 'found';
$strict = array_search('pear', $items) === false ? 'not found' : 'found';

echo 'loose: ', $loose, "\n";
echo 'strict: ', $strict, "\n";
echo 'really absent, loose: ',
    (array_search('apple', $items) == false ? 'not found' : 'found'), "\n";

$mixed = array_filter([1, 0, 2, 0, 3]);
echo 'zeros dropped: ', json_encode($mixed), "\n";
echo 'keys: ', implode(',', array_keys($mixed)), "\n";
loose: not found
strict: found
really absent, loose: not found
zeros dropped: {"0":1,"2":2,"4":3}
keys: 0,2,4

The loose comparison that searched for the first element gave the result not found, and the strict comparison gave found; on an element that is truly absent, the loose comparison gave the correct result. The same call answers wrongly when the sought element is first in the list and correctly when it is elsewhere, and no channel reports this difference. The discriminating power of the return channel is limited by the discriminating power of the caller: if 0 and false are not told apart, the first element found counts as “absent”.

When array_filter is called without a callback, every element whose value is equal to false is dropped; 0 is one of them. The keys in the output stay 0, 2, 4: data is lost and the array stops being indexed, and both are silent. If filtering is what was intended, there is no problem; if the aim was only to drop empty values, 0 is dropped too, though it was an expected value.

Summary

  • array_filter keeps keys, array_map carries keys along, and array_reduce comes down to a single value with an initial value.
  • After filtering, an array can stop being indexed; array_values renumbers the keys.
  • array_search returns false when it finds nothing; the measuring channel of this lesson is return (element not found).
  • An invalid argument value (str_repeat('a', -1), array_combine with mismatched lengths) throws ValueError; the channel is halts.
  • The 0 returned when the first element is found is the same as false in a loose comparison; only a strict comparison separates the two.
  • array_filter without a callback silently drops every element equal to false, 0 included.

Next Step

This lesson measured the discriminating power of the return channel and showed that, once 0 and false are mixed up, the distinction is left to the caller. The String Handling lesson measures a harsher case of the same channel: if two separate problems give the same return value, the value alone cannot say which of them it is.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close