Tying Arrays
A class implementing a tied array must define at least the methods TIEARRAY
, FETCH
, and STORE
. There are many optional methods: the ubiquitous DESTROY
method, of course, but also the STORESIZE
and FETCHSIZE
methods used to provide $#array
and scalar(@array)
access. In addition, CLEAR
is triggered when Perl needs to empty the array, and EXTEND
when Perl would have pre-extended allocation in a real array.
You may also define the POP
, PUSH
, SHIFT
, UNSHIFT
, SPLICE
, DELETE
, and EXISTS
methods if you want the corresponding Perl functions to work on the tied array. The Tie::Array
class can serve as a base class to implement the first five of those functions in terms of FETCH
and STORE
. (Tie::Array
's default implementation of DELETE
and EXISTS
simply calls croak
.) As long as you define FETCH
and STORE
, it doesn't matter what kind of data structure your object contains.
On the other hand, the Tie::StdArray
class (defined in the standard Tie::Array
module) provides a base class with default methods that assume the object contains a regular array. Here's a simple array-tying class that makes use of this. Because it uses Tie::StdArray
as its base class, it only needs to define the methods that should be treated in a nonstandard way.
#!/usr/bin/perl package ClockArray; use Tie::Array; our @ISA = 'Tie::StdArray'; sub FETCH { my($self,$place) = @_; $self->[ $place % 12 ]; } sub STORE { my($self,$place,$value) = @_; $self->[ $place % 12 ] = $value; } package main; tie my @array, 'ClockArray'; @array = ( "a" ... "z" ); print "@array\n";
When run, the program prints out "
y z o p q r s t u v w x
". This class provides an array with only a dozen slots, like hours of a clock, numbered 0 through 11. If you ask for the 15th array index, you really get the 3rd one. Think of it as a travel aid for people who haven't learned how to read 24-hour clocks.
Array-Tying Methods
That's the simple way. Now for some nitty-gritty details. To demonstrate, we'll implement an array whose bounds are fixed at its creation. If you try to access anything beyond those bounds, an exception is raised. For example:
use BoundedArray; tie @array, "BoundedArray", 2; $array[0] = "fine"; $array[1] = "good"; $array[2] = "great"; $array[3] = "whoa"; # Prohibited; displays an error message.
The preamble code for the class is as follows:
package BoundedArray; use Carp; use strict;
To avoid having to define
SPLICE
later, we'll inherit from the Tie::Array
class:
use Tie::Array; our @ISA = ("Tie::Array");
- CLASSNAME
->TIEARRAY(
LIST)
- As the constructor for the class,
TIEARRAY
should return a blessed reference through which the tied array will be emulated.In this next example, just to show you that you don't really have to return an array reference, we'll choose a hash reference to represent our object. A hash works out well as a generic record type: the value in the hash's "
BOUND
" key will store the maximum bound allowed, and its "DATA
" value will hold the actual data. If someone outside the class tries to dereference the object returned (doubtless thinking it an array reference), an exception is raised.sub TIEARRAY { my $class = shift; my $bound = shift; confess "usage: tie(\@ary, 'BoundedArray', max_subscript)" if @_ || $bound =~ /\D/; return bless { BOUND => $bound, DATA => [] }, $class; }
We can now say:
tie(@array, "BoundedArray", 3); # maximum allowable index is 3
to ensure that the array will never have more than four elements. Whenever an individual element of the array is accessed or stored,FETCH
andSTORE
will be called just as they were for scalars, but with an extra index argument. - SELF
->FETCH(
INDEX)
- This method is run whenever an individual element in the tied array is accessed. It receives one argument after the object: the index of the value we're trying to fetch.
sub FETCH { my ($self, $index) = @_; if ($index > $self->{BOUND}) { confess "Array OOB: $index > $self->{BOUND}"; } return $self->{DATA}[$index]; }
- SELF
->STORE(
INDEX VALUE)
- This method is invoked whenever an element in the tied array is set. It takes two arguments after the object: the index at which we're trying to store something and the value we're trying to put there. For example:
sub STORE { my($self, $index, $value) = @_; if ($index > $self->{BOUND} ) { confess "Array OOB: $index > $self->{BOUND}"; } return $self->{DATA}[$index] = $value; }
- SELF
->DESTROY
- Perl calls this method when the tied variable needs to be destroyed and its memory reclaimed. This is almost never needed in a language with garbage collection, so for this example we'll just leave it out.
- SELF
->FETCHSIZE
- The
FETCHSIZE
method should return the total number of items in the tied array associated with SELF. It's equivalent toscalar(@array)
, which is usually equal to$#array + 1
.
sub FETCHSIZE { my $self = shift; return scalar @{$self->{DATA}}; }
- SELF
->STORESIZE(
COUNT)
- This method sets the total number of items in the tied array associated with SELF to be COUNT. If the array shrinks, you should remove entries beyond COUNT. If the array grows, you should make sure the new positions are undefined. For our
BoundedArray
class, we also ensure that the array doesn't grow beyond the limit initially set.
sub STORESIZE { my ($self, $count) = @_; if ($count > $self->{BOUND}) { confess "Array OOB: $count > $self->{BOUND}"; } $#{$self->{DATA}} = $count; }
- SELF
->EXTEND(
COUNT)
- Perl uses the
EXTEND
method to indicate that the array is likely to expand to hold COUNT entries. That way you can can allocate memory in one big chunk instead of in many successive calls later on. Since ourBoundedArray
s have fixed upper bounds, we won't define this method. - SELF
->EXISTS(
INDEX)
- This method verifies that the element at INDEX exists in the tied array. For our
BoundedArray
, we just employ Perl's built-inexists
after verifying that it's not an attempt to look past the fixed upper bound.
sub EXISTS { my ($self, $index) = @_; if ($index > $self->{BOUND}) { confess "Array OOB: $index > $self->{BOUND}"; } exists $self->{DATA}[$index]; }
- SELF
->DELETE(
INDEX)
- The
DELETE
method removes the element at INDEX from the tied array SELF. For ourBoundedArray
class, the method looks nearly identical toEXISTS
, but this is not the norm.
sub DELETE { my ($self, $index) = @_; print STDERR "deleting!\n"; if ($index > $self->{BOUND}) { confess "Array OOB: $index > $self->{BOUND}"; } delete $self->{DATA}[$index]; }
- SELF
->CLEAR
- This method is called whenever the array has to be emptied. That happens when the array is set to a list of new values (or an empty list), but not when it's provided to the
undef
function. Since a clearedBoundedArray
always satisfies the upper bound, we don't need check anything here:
sub CLEAR { my $self = shift; $self->{DATA} = []; }
If you set the array to a list,CLEAR
will trigger but won't see the list values. So if you violate the upper bound like so:
tie(@array, "BoundedArray", 2); @array = (1, 2, 3, 4);
theCLEAR
method will still return successfully. The exception will only be raised on the subsequentSTORE
. The assignment triggers oneCLEAR
and fourSTORE
s. - SELF
->PUSH(
LIST)
- This method appends the elements of LIST to the array. Here's how it might look for our
BoundedArray
class:
sub PUSH { my $self = shift; if (@_ + $#{$self->{DATA}} > $self->{BOUND}) { confess "Attempt to push too many elements"; } push @{$self->{DATA}}, @_; }
- SELF
->UNSHIFT(
LIST)
- This method prepends the elements of LIST to the array. For our
BoundedArray
class, the subroutine would be similar toPUSH
. - SELF
->POP
- The
POP
method removes the last element of the array and returns it. ForBoundedArray
, it's a one-liner:
sub POP { my $self = shift; pop @{$self->{DATA}} }
- SELF
->SHIFT
- The
SHIFT
method removes the first element of the array and returns it. ForBoundedArray
, it's similar toPOP
. - SELF
->SPLICE(
OFFSET LENGTH LIST)
- This method lets you splice the SELF array. To mimic Perl's built-in
splice
, OFFSET should be optional and default to zero, with negative values counting back from the end of the array. LENGTH should also be optional, defaulting to rest of the array. LIST can be empty. If it's properly mimicking the built-in, the method will return a list of the original LENGTH elements at OFFSET (that is, the list of elements to be replaced byLIST
).Since splicing is a somewhat complicated operation, we won't define it at all; we'll just use the
SPLICE
subroutine from theTie::Array
module that we got for free when we inherited fromTie::Array
. This way we defineSPLICE
in terms of otherBoundedArray
methods, so the bounds checking will still occur.
That completes our BoundedArray
class. It warps the semantics of arrays just a little. But we can do better, and in very much less space.
Notational Convenience
One of the nice things about variables is that they interpolate. One of the not-so-nice things about functions is that they don't. You can use a tied array to make a function that can be interpolated. Suppose you want to interpolate random integers in a string. You can just say:
#!/usr/bin/perl package RandInterp; sub TIEARRAY { bless \my $self }; sub FETCH { int rand $_[1] }; package main; tie @rand, "RandInterp"; for (1,10,100,1000) { print "A random integer less than $_ would be $rand[$_]\n"; } $rand[32] = 5; # Will this reformat our system disk?
When run, this prints:
A random integer less than 1 would be 0 A random integer less than 10 would be 3 A random integer less than 100 would be 46 A random integer less than 1000 would be 755 Can't locate object method "STORE" via package "RandInterp" at foo line 10.
As you can see, it's no big deal that we didn't even implement
STORE
. It just blows up like normal.