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 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:

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, pop, shift, unshift, splice, delete, and 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 @aPOP; shift @aSHIFT; unshift @a, 0UNSHIFT.

  • 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] = $vSTORE.

  • 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:

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 and exists.

See also#

  • tie - the built-in and the full array method menu

  • tied - reach the backing object

  • splice - the most intricate mutator the SPLICE hook stands in for

  • push and pop - the mutators Tie::Array derives from the mandatory layer

  • Tied hashes - the other rich container interface

  • Tie::Array - the base-class module that supplies the list mutators