Skip to content
academia.sh

Lesson 07 / 14

Indexed and Associative Arrays

One array structure used as both a list and a dictionary; the keys '1', 1 and true collapsing into a single key; a decimal key being truncated; normalization applying only to canonical decimal integer strings.

Published Updated

In this lesson

  • Demonstrates, with an example, that one array structure serves as both an indexed and an associative array and keeps insertion order
  • Measures the collapse of the keys '1', 1 and true into a single key as a silent channel and classifies it
  • Measures which strings key normalization applies to and ties the result to the canonical decimal integer rule
Contents

The Functions lesson measured an extra argument being discarded without any signal: add_two(1, 2, 3) produced 3 and no word of it reached the caller. This lesson asks whether the same silence appears in a data structure: when the same array receives keys written in different forms and they run together, who says so? The course’s measuring question does not change, which is which channel the problem is heard through, and the channel names are only used in this lesson: silent, return, warning, halts.

In PHP, array is a single structure that serves two uses. An array whose keys are consecutive integers reads as an indexed array, and an array whose keys are strings reads as an associative array. There is no type difference between them; the difference lies only in what the keys are. The variable and type concepts built in the Programming Fundamentals course (M01/K02) apply here as they stand. What is new is that a key is not stored as written; it passes through a rule first.

Two Uses in One Structure

The file below uses the same structure in three ways: as a list, as a dictionary, and as a mixture of the two. The keys of each array are printed side by side with array_keys.

<?php
// arrays.php — the same structure serves as both a list and a dictionary
$items = ['apple', 'pear', 'quince'];
$items[] = 'pomegranate';

$person = ['name' => 'Ada', 'age' => 36];
$person['city'] = 'Izmir';

$mixed = ['one', 'two', 'special' => 'x'];
$mixed[] = 'three';

$ordered = [3 => 'c', 1 => 'a'];
$ordered[] = 'd';

$removed = ['x', 'y', 'z'];
unset($removed[1]);
$removed[] = 'w';

echo 'items: ', implode(',', array_keys($items)), "\n";
echo 'person: ', implode(',', array_keys($person)), "\n";
echo 'mixed: ', implode(',', array_keys($mixed)), "\n";
echo 'ordered: ', implode(',', array_keys($ordered)), "\n";
echo 'removed: ', implode(',', array_keys($removed)), "\n";
echo 'is list: ', var_export(array_is_list($items), true), ' ',
    var_export(array_is_list($person), true), ' ',
    var_export(array_is_list($removed), true), "\n";
items: 0,1,2,3
person: name,age,city
mixed: 0,1,special,2
ordered: 3,1,4
removed: 0,2,3
is list: true false false

Appending with square brackets adds an element without a key, and PHP gives it the next integer key: in the mixed array, the value 'three' added after the special key received the key 2. The ordered line shows that order depends on insertion and not on the key: the key 3 comes first because it was written first, followed by 1 and the appended 4. The removed line shows that keys are not renumbered when an element is deleted, which is why array_is_list returns false. An array that looks indexed can stop being indexed after an unset, and nothing reports it.

AS1. In this lesson, “order” means the insertion order of the elements; foreach walks over the array in that order. The run above shows that order is independent of key values, and any job that needs numeric order sorts explicitly. Whether an array is copied when it is assigned to another variable is not the question of this lesson: that is the “who sees this change” question measured in M11/K03, and it is not asked here.

Key Normalization

An array key can only be an int or a string. A key written with any other type is normalized to one of these two: a true key becomes 1, a decimal number is truncated to an integer, and a string in decimal integer form becomes an integer. That is the whole definition; what matters is that the conversion is invisible at the level of the written code. The file below pulls in channel.php; the channel classifier is the shared helper of the course, and its cross-section is used in this lesson.

<?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
// normalization.php — differently written keys collapsing into one key
require 'channel.php';

$observations = [
    'three forms, one key' => fn() => ['1' => 'a', 1 => 'b', true => 'c'],
    'decimal key'          => function () {
        $key = 1.7;
        return [$key => 'x'];
    },
];

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

$array = ['1' => 'a', 1 => 'b', true => 'c'];
echo 'remaining key: ', array_key_first($array), ', value: ', $array[1], "\n";
three forms, one key  silent   result array(1)
decimal key           warning  E_DEPRECATED, result array(1)
remaining key: 1, value: c

The first line is the course’s key normalization observation, and it belongs to the silent channel. The keys '1', 1 and true are three separate spellings; all three landed on the key 1, one element remained in the array, and the value of that element is the last one written, 'c'. listen() asked its questions in order: nothing was thrown, no warning was reported, and the result is neither false nor null; silence is what remains. The problem can be seen only by looking at the result. Someone who expected array(3) here instead of array(1) heard nothing about the two missing elements. This is the same channel as the discarded extra argument; the difference is that what is discarded here is a value, and the key overwrites it.

The second line shows another face of the same mechanism: the key 1.7 was truncated to 1, and this time an error level was reported. E_DEPRECATED is the deprecation level, and execution continues; the channel is therefore warning. The keys true and '1' reach 1 without losing a value, whereas 1.7 drops its fractional part. The limit of the silence is drawn by information loss: a conversion that loses information is announced, and a conversion that appears lossless is not.

AS2. In this lesson E_DEPRECATED is recorded under the warning channel, because the definition in the core asks only two things: was an error level reported, and did execution continue. The meaning of the level, that the behavior may change in the future, does not alter the channel; it only says that this warning may become a halts someday. The lesson does not measure that future.

Key Types and Merging

Normalization does not work only between strings and integers; Boolean values, the empty value and decimal numbers can also be written in place of a key. To measure which key each one lands on, every candidate is held in a variable and used as the key of a one-element array.

<?php
// types.php — the key that candidates of different types land on
require 'channel.php';

function key_of(mixed $candidate): string
{
    $single = [$candidate => 'x'];
    return var_export(array_key_first($single), true);
}

$candidates = [
    'null'        => null,
    'false'       => false,
    'true'        => true,
    'whole float' => 2.0,
    'empty string' => '',
];

foreach ($candidates as $name => $candidate) {
    [$channel, $evidence] = listen(fn() => key_of($candidate));
    printf("%-14s%-9s%s\n", $name, $channel, $evidence);
}

[$channel, $evidence] = listen(function () {
    $none = null;
    return count([$none => 'a', '' => 'b']);
});
echo 'null and empty string together: ', $channel, ' ', $evidence, "\n";
null          warning  E_DEPRECATED, result string ''
false         silent   result string 0
true          silent   result string 1
whole float   silent   result string 2
empty string  silent   result string ''
null and empty string together: warning E_DEPRECATED, result int 1

A null key lands on the empty string and reports E_DEPRECATED (warning); false lands on 0, true on 1, and 2.0, which has no fraction, on 2, and these three, together with the empty string, are silent. The last line ties the result together: null and '' are the same key, and an array that writes both carries a single element. Most of these conversions look free of information loss, yet a source that carries null and the empty string as separate data loses that distinction once the data enters the array. A decimal number with a fraction, as measured in the previous section, is truncated with a warning.

AS3. The five candidates in the table illustrate the language’s conversion rule; types that are not valid as keys (another array, an object) are outside this lesson and are not measured here. The lesson reads which type lands on which key from the executed output, not from the written rule.

When two arrays are merged, a key collision is a second point of normalization. Three different merges applied to the same two arrays produce three different fates for the keys.

<?php
// merging.php — merging the same two arrays in three different ways
$a = ['name' => 'Ada', 5 => 'x', 6 => 'y'];
$b = ['name' => 'Can', 5 => 'z'];

echo 'addition: ', json_encode($a + $b), "\n";
echo 'array_merge: ', json_encode(array_merge($a, $b)), "\n";
echo 'array_replace: ', json_encode(array_replace($a, $b)), "\n";
echo 'spread: ', json_encode([...$a, ...$b]), "\n";
echo 'keys: ', implode(',', array_keys(array_merge($a, $b))), "\n";
addition: {"name":"Ada","5":"x","6":"y"}
array_merge: {"name":"Can","0":"x","1":"y","2":"z"}
array_replace: {"name":"Can","5":"z","6":"y"}
spread: {"name":"Can","0":"x","1":"y","2":"z"}
keys: name,0,1,2

The addition operator keeps the values of the left array and adds only the new keys from the right: the name key stayed 'Ada' and the 5 key stayed 'x'. array_merge takes the right value for string keys ('Can') but renumbers integer keys: the keys 5 and 6 disappeared, and all three elements from the two sides were kept under the keys 0, 1, 2. array_replace, by contrast, writes the right value without touching the integer keys. The spread syntax gives the same result as array_merge. Four spellings produced three different results for the same two arrays, and none of them reported a level: the channel is silent.

AS4. The difference between array_merge and addition is measured in this lesson only through key collisions. Which is faster, or which should be preferred, is not measured; the lesson does not rank one function above the other, and only shows that the same input gives different results.

The Does-the-Key-Exist Question

Whether a key is present in an array can be asked with two separate questions, and the questions do not give the same answer. isset asks whether the key exists and its value is not null, array_key_exists asks only whether the key exists, and ?? supplies the fallback when the value is missing or null.

<?php
// exists.php — the difference between isset, array_key_exists and ??
$data = ['full' => 'x', 'empty' => null, 'zero' => 0];

foreach (['full', 'empty', 'zero', 'absent'] as $key) {
    printf(
        "%-6s isset=%-5s exists=%-5s coalesce=%s\n",
        $key,
        var_export(isset($data[$key]), true),
        var_export(array_key_exists($key, $data), true),
        var_export($data[$key] ?? 'fallback', true)
    );
}
full   isset=true  exists=true  coalesce='x'
empty  isset=false exists=true  coalesce='fallback'
zero   isset=true  exists=true  coalesce=0
absent isset=false exists=false coalesce='fallback'

The empty line shows the distinction: the key exists, and the value is null. isset treats it as missing, array_key_exists treats it as present, and ?? supplies the fallback. On the zero line, all three accept the value as present; 0 is not an empty value. On the absent line, where the key truly does not exist, isset and array_key_exists both give false. All three queries returned a bool or a fallback value without raising a warning; code that wants to hear about a missing key from a warning must choose the plain read, and the plain read is measured in the next lesson.

The Bounding Measurement

Normalization is not applied to every string. To measure which strings are converted to an integer key, string keys can be tried one by one: each is made the key of a one-element array, and array_key_first reports the type of that key.

<?php
// bounds.php — which string keys are converted to integers
$candidates = ['0', '1', '-1', '01', '1.5', '-0', ' 1'];

foreach ($candidates as $candidate) {
    $single = [$candidate => 'x'];
    $key = array_key_first($single);
    printf("%-6s%s\n", "'" . $candidate . "'", get_debug_type($key));
}

$two = ['1' => 'a', '01' => 'b'];
echo 'two spellings, element count: ', count($two), "\n";
'0'   int
'1'   int
'-1'  int
'01'  string
'1.5' string
'-0'  string
' 1'  string
two spellings, element count: 2

The rule is read from the output: only strings in decimal, canonical integer form are converted to an integer key. '1' and '-1' become integers; strings with a leading zero ('01'), a decimal point ('1.5'), a minus-signed zero ('-0') or a space (' 1') stay strings. This distinction draws the limit of the silence in the previous section: if '1' and 1 are the same key, '01' is a different key, and the two spellings on the last line leave two elements this time. Normalization is not a rule of “merging whatever writes the same number differently”; it is a rule of “converting the canonical spelling to an integer”. A data source that writes the same number in two forms produces two separate elements in the array, and that too is reported by no signal.

AS5. The types and keys in this section are the result of the language’s own rule and rest on a class, not on a single number. The seven candidates measured illustrate the class the language documents and do not claim to be exhaustive. If a string key is not converted to an integer, the reason is that the string is not in canonical decimal form; the lesson does not move on to other bases or to very large integers.

Summary

  • In PHP, array is a single structure: an array whose keys are consecutive integers reads as indexed, and one with string keys reads as associative; order is insertion order.
  • unset does not renumber keys; an array that looks indexed does not report whether it is still indexed unless it is tested with array_is_list.
  • '1', 1 and true normalize to a single key and one element remains in the array; the measuring channel of this lesson is silent (key normalization).
  • A decimal key such as 1.7 is truncated and E_DEPRECATED is reported; a conversion that loses information is heard through the warning channel.
  • Normalization applies only to strings in decimal, canonical integer form; '01', '1.5' and '-0' stay strings.

Next Step

This lesson measured the keys of a single-level array. When an element of an array is itself another array, the key is no longer a single value but a path, and if one link of the path is missing, null arrives. The Multidimensional Arrays lesson measures which channel a missing inner key is heard through, and why writing to the same path is heard through a different channel.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close