# Tied arrays A tied array has two layers of interface. The lower layer - `FETCH`, `STORE`, `FETCHSIZE`, `STORESIZE`, `CLEAR` - is mandatory and handles element access and resizing. The upper layer - `PUSH`, `POP`, `SHIFT`, `UNSHIFT`, `SPLICE`, `DELETE`, `EXISTS` - is optional and lets the array respond to the matching built-ins; without a given hook, that built-in either falls back to the lower layer or dies. The full menu is on the [`tie`](../../p5/core/perlfunc/tie) reference page. This chapter builds a class, then explains the size protocol and the optional mutators, with their call timing verified against the running interpreter. ## A complete class A numeric array whose empty slots read back as `0` instead of `undef`, and whose `STORESIZE` pads new slots with `0`: ```perl use v5.36; package ZeroFill { sub TIEARRAY { my $class = shift; return bless { data => [] }, $class } sub FETCH { my ($self, $i) = @_; return $self->{data}[$i] // 0 } sub STORE { my ($self, $i, $v) = @_; $self->{data}[$i] = $v } sub FETCHSIZE { my $self = shift; return scalar @{ $self->{data} } } sub STORESIZE { my ($self, $n) = @_; my $data = $self->{data}; if ($n > @$data) { push @$data, (0) x ($n - @$data); # pad with 0, not undef } else { $#$data = $n - 1; } } sub CLEAR { my $self = shift; $self->{data} = [] } sub PUSH { my $self = shift; push @{ $self->{data} }, @_; return scalar @{ $self->{data} } } sub POP { my $self = shift; return pop @{ $self->{data} } } } tie my @a, 'ZeroFill'; $a[0] = 11; $a[2] = 33; # FETCH on the skipped slot 1 reads back 0 say scalar @a; # 3 say $a[1]; # 0 $#a = 4; # STORESIZE grows the array, padding with 0 say "@a"; # 11 0 33 0 0 push @a, 44; say pop @a; # 44 ``` The object is a blessed hash ref wrapping a real array in its `data` slot - a common pattern, because the class usually wants room for state beyond the array itself. ## The mandatory layer `TIEARRAY classname, LIST` : The constructor; returns a blessed reference that becomes the tied object. `FETCH this, index` / `STORE this, index, value` : Single-element read and write. A negative index is translated to a positive one - the runtime calls `FETCHSIZE` first and adds it to the index - unless the class sets `$NEGATIVE_INDICES` to a true value, in which case the raw negative index reaches `FETCH` and `STORE`. `FETCHSIZE this` : Returns the number of elements. This backs `scalar @array` and the high-water mark for `$#array`. It is the single most-called method on a tied array, since the runtime needs the size to normalise negative indices and to bound iteration. `STORESIZE this, count` : Sets the array's length. Growing should fill the new slots with the class's notion of "empty" (here, `0`); shrinking should drop the excess. This backs `$#array = N` and assignment-driven resizing. `CLEAR this` : Empties the array - triggered by `@array = ()`. ## The size protocol in practice `FETCHSIZE` and `STORESIZE` are where tied arrays differ most from tied hashes. Three things to keep straight: - **`$#a` reads through `FETCHSIZE` and writes through `STORESIZE`.** Setting `$#a = 4` calls `STORESIZE($obj, 5)`. - **Skipped slots are the class's problem.** Assigning `$a[2]` when the array has length 1 calls `STORE($obj, 2, ...)` directly - the runtime does *not* call `STORESIZE` to fill the gap. If a read of slot 1 should return `0` rather than `undef`, `FETCH` has to say so (the `// 0` in the example). - **`EXTEND` is advisory.** When the runtime expects an array to grow - for instance just before a large assignment - it may call `EXTEND this, count` as a hint to pre-allocate. It is always safe to leave `EXTEND` undefined or empty; it is purely an optimisation opportunity, and making it behave like `STORESIZE` is a bug, because the runtime calls `EXTEND` at times when it does not actually want the visible length to change. ## The optional mutators Each of [`push`](../../p5/core/perlfunc/push), [`pop`](../../p5/core/perlfunc/pop), [`shift`](../../p5/core/perlfunc/shift), [`unshift`](../../p5/core/perlfunc/unshift), [`splice`](../../p5/core/perlfunc/splice), [`delete`](../../p5/core/perlfunc/delete), and [`exists`](../../p5/core/perlfunc/exists) dispatches to the uppercase hook of the same name when the class defines it. Their call timing, confirmed against the interpreter: - `push @a, 1, 2` → one `PUSH` call with the whole list. - `pop @a` → `POP`; `shift @a` → `SHIFT`; `unshift @a, 0` → `UNSHIFT`. - `splice @a, 1, 1, 'x', 'y'` → one `SPLICE` call carrying offset, length, and the replacement list; its return value becomes `splice`'s. - `exists $a[0]` → `EXISTS`; `delete $a[0]` → `DELETE`. When a mutator hook is **absent**, behaviour depends on the operator. `Tie::Array` (below) supplies fallbacks for the common five in terms of the mandatory layer; without that base class, calling an unimplemented mutator dies at the point of use. ## Call timing - `$a[$i]` → `FETCH`; `$a[$i] = $v` → `STORE`. - `scalar @a`, normalising a negative index → `FETCHSIZE`. - `$#a = $n`, `@a = (...)` resizing → `STORESIZE` (and `CLEAR` first for whole-array assignment). - `push`/`pop`/`shift`/`unshift`/`splice`/`delete`/`exists` → the matching uppercase hook when defined. - An array slice `@a[1, 3]` calls `FETCH` once per index; there is no slice hook. ## Minimum viable class The smallest tied array defines `TIEARRAY`, `FETCH`, `STORE`, `FETCHSIZE`, and `STORESIZE`. With those five, element access, `scalar @a`, `$#a`, and iteration all work. Add `CLEAR` so `@a = ()` behaves. Add the mutators only for the built-ins your callers actually use - or inherit them. ## The easy way: inherit from `Tie::Array` `Tie::Array` implements `PUSH`, `POP`, `SHIFT`, `UNSHIFT`, and `SPLICE` in terms of the mandatory five, so a subclass that supplies `TIEARRAY`, `FETCH`, `STORE`, `FETCHSIZE`, and `STORESIZE` gets all the list mutators for free: ```perl package Logging { use Tie::Array; our @ISA = ('Tie::Array'); sub TIEARRAY { my $class = shift; return bless { data => [] }, $class } sub FETCH { my ($self, $i) = @_; return $self->{data}[$i] } sub STORE { my ($self, $i, $v) = @_; warn "store $i\n"; $self->{data}[$i] = $v } sub FETCHSIZE { my $self = shift; return scalar @{ $self->{data} } } sub STORESIZE { my ($self, $n) = @_; $#{ $self->{data} } = $n - 1 } } tie my @log, 'Logging'; push @log, 'x', 'y'; # PUSH inherited; calls STORE twice (warns) say "@log"; # x y ``` `push @log, 'x', 'y'` works without a `PUSH` method because the inherited one expands into two `STORE` calls - which is why the example warns twice. `Tie::Array` does not define `TIEARRAY` itself, so the subclass must supply the constructor; its default `DELETE` and `EXISTS` simply croak, so override them if your callers need [`delete`](../../p5/core/perlfunc/delete) and [`exists`](../../p5/core/perlfunc/exists). ## See also - [`tie`](../../p5/core/perlfunc/tie) - the built-in and the full array method menu - [`tied`](../../p5/core/perlfunc/tied) - reach the backing object - [`splice`](../../p5/core/perlfunc/splice) - the most intricate mutator the `SPLICE` hook stands in for - [`push`](../../p5/core/perlfunc/push) and [`pop`](../../p5/core/perlfunc/pop) - the mutators `Tie::Array` derives from the mandatory layer - [Tied hashes](hashes) - the other rich container interface - `Tie::Array` - the base-class module that supplies the list mutators