Lesson 7

Arrays of hashes of arrays of hashrefs

Creating arrays and hashes is all very well, but sometimes you want something a bit less flat, a little more multidimensional. Well, Perl does indeed allow for multidimensional arrays, but the syntax is a little odd. Let us enter the world of the reference.

References are like signposts to other data. To create a multidimensional array, what you actually need to do is create a normal 1 dimensional array whose elements are signposts to other arrays. In this way we we can fake a multidimensional array. So, how do we create a reference? There are two ways. The first is to use the \ operator, which creates references to bits of data you have named and defined previously:

$scalar     = "hello";
$scalar_ref = \$scalar;
@array      = ( qw{q w e rt y} );
$array_ref  = \@array;
%hash       = ( key => "value", lemon => "curd" );
$hash_ref   = \%hash;

Because references are just a single signpost saying "the thing I refer to is just over there", they are $ingular data, hence they are always $calars. Creating arrayrefs and hashrefs is such a common thing to do, there is a shorthand, which allows you to create them directly, and without naming them:

$array_ref  = [ "q", "w", "e", "r", "t", "y" ];
$array_ref2 = [ qw{ you can use qw to save time } ];
$hash_ref   = { key => "value", lemon => "curd" };

These create references to anonymous arrays and anonymous hashes. In the first example, we created references to named arrays and hashes, like @array and %hash. In this example, there is no named array or named hash, only the [ ] brackets or { } braces, which are never named.

You can also create references to anonymous subroutines:

$code_ref = sub { return 2**$_[0] };

Which is very useful, as we shall see. Creating references is easy then: you just need the right mix of \, sub and { } [ ] braces and brackets. How do we get back to the contents? The posh word for this is dereferencing. To see how this works, let's take an example of the multidimensional array we wanted in the first place:

@unidimensional =
(
    ( "this is just a one dimensional array", "even though"),
    ( "we have nested", "parentheses" )
);
    # parentheses don't nest in perl:
    # everything gets flattened to a single list
@multidimensional =
(
    [ "but this one", "really is multidimensional" ],
    [ "two by", "two" ]
);
    # brackets mean create references to anonymous arrays
    # and put them in @multidimensional

The multidimensional array is really a one-dimensional array that contains pointers (references) to the location of other arrays. So Perl doesn't really have multidimensional arrays, but you can fake them using references. Getting at the data is a little complex, however:

#!/usr/bin/perl
use strict;
use warnings;
my @multidimensional =
(
    [ "but this one", "really is multidimensional" ],
    [ "two by", "two" ]
);
my $element_1_1 = $multidimensional[1]->[1]; # gets 'two'
print $element_1_1;
two

The $multidimensional[1] bit is obvious: we're just using the usual (counting from zero) syntax for pulling out the 2nd element of the array. If we actually captured this:

#!/usr/bin/perl
use strict;
use warnings;
my @multidimensional =
(
    [ "but this one", "really is multidimensional" ],
    [ "two by", "two" ]
);
$element_1 = $multidimensional[1];
print ref $element_1;
print $element_1;

the scalar $element_1 would contain the reference itself. We can use the ref operator to find out what the scalar points to:

print ref $element_1;
ARRAY

We can also find out what perl calls the reference:

print $element_1;
ARRAY(0x183f1dc)

or similar, which isn't very informative! The 0x... is a hexadecimal code to the array's location in memory, basically the 'map reference' where perl has stored the array it points to. So although:

$multidimensional[1]

returns a reference to an array ('0x183f1dc'):

->[1]

is the bit that actually 'dereferences' the reference, chasing the pointer to the array at 0x183f1dc. You could think of the -> as 'follow this signpost to the real data'. The -> dereferencing arrow can also be used to pull out bits of multidimensional hashes:

#!/usr/bin/perl
use strict;
use warnings;
my %multi_hash =
(
    name =>
    {
        fore => "Steve",
        sur  => "Cook",
    },
    age => 26
);
my $forename = $multi_hash{name}->{fore};
my $age      = $multi_hash{age};
print "name $forename, age $age\n";
name Steve, age 26

You can see here that a multidimensional structure needn't be a boring rectangular matrix, exactly 2 by 2 or 4 by 4 by 3: it can be unbalanced and weird: you can have anything you like:

#!/usr/bin/perl
use strict;
use warnings;
my @complex =
(
    "an array",
    {
        containing => "a hashref"
    },
    "a scalar",
    [ # element 3 of the @complex is this big arrayref
        [
            "a two deep arrayref",
            "with two elements"
        ],
        { # element 1 of this arrayref is a little hashref
            lemon => "curd" # key lemon of this hashref is curd
        }
    ]
);
print my $wanted = $complex[3]->[1]->{lemon};

Hopefully the syntax should be OK now: if you wanted the words "curd", you need:

my $wanted = $complex[3]->[1]->{lemon};

i.e. take the third element of @complex (don't forget we count from 0!), that is the big arrayref at the end, dereference it to get the first element (the little hashref inside the arrayref), then dereference the hashref with the key lemon.

When we talk about objects later, we'll find that they are usually anonymous hashrefs:

$anonymous_hashref = { name => "Cornelia", species => "Elaphe guttata" };

to get at the name this time, we need the syntax:

$name = $anonymous_hashref->{name};

Note that because we are starting with a scalar (not an array as in the last example), the very first thing we need to do is dereference it with an arrow: the scalar $anonymous_hashref points to an (anonymous) hash, so you can't just look at its keys, because a scalar doesn't have keys. If this doesn't make sense, compare these:

@array    = ( qw/ some elements/ );
$zeroeth_element_of_array    = $array[0];
$arrayref = [ qw/some elements/ ];
$zeroeth_element_of_arrayref = $arrayref->[0];

There is a certain shortcut you can take when dealing with multidimensional arrays and hashes (and mixtures, like arrays of arrayrefs of hashrefs). Since perl knows that an array or hash can only contain a single 1 dimensional list, it knows that:

@array = ( [ "two", "by" ], [ "two", "elements" ] );
$element_0_1 = $element[0][1];

can only mean:

$element_0_1 = $array[0]->[1];

So, in general, if you're dealing with something that is a real array or hash at the highest level, you can miss off the -> arrows completely unambiguously, so our earlier example:

$wanted = $complex[3]->[1]->{lemon};

could be written with less line noise as:

$wanted = $complex[3][1]{lemon};

However, you must be careful with this syntax if you're dealing with something that is a scalar at the highest level (i.e. an anonymous hashref or arrayref):

$arrayref    = [ [ "two", "by" ], [ "two", "elements" ] ];
    # note this is scalar (an arrayref), not an array
$element_0_1 = $arrayref->[0]->[1];
$element_0_1 = $arrayref->[0][1];

The last two are equivalent, but note you can't dispense with the first -> , which is actually obvious, as:

$element[0][1];

means the first element of the zeroeth arrayref contained in @element, which doesn't exist (or if it does, you'll get the value from that, which isn't what you're after).

You've now seen that constructing references can be done in many ways. TIMTOWTDI. Some examples:

$arrayref  = [ 0, 1, 2, 3 ];
    # create a scalar that is an anonymous arrayref
@array     = ( 0, 1, 2, 3 );
$arrayref  = \@array;
    # create a reference to a real named array
@multi     = ( \@one, \@two, \@three );
    # multidimensional array constructed from references to named arrays
@multi2    = ( [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] );
    # multidimensional array constructed from anonymous arrayrefs
$arrayref2 = [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ];
    # multidimensional: arrayref constructed from anonymous arrayrefs

Now, oftentimes, you won't want to play with just a single element of a thingy (which is the not very descriptive term for a perl data structure composed of some gungy mass of references). You'll want a slice of a thingy, or to iterate over the whole array pointed to by an arrayref. How do we do this? Well, both need the same sort of dereferencing syntax:

$arrayref = [ 0, 1, 2, 3, 4 ];
@slice    = @{ $arrayref }[ 0, 2 ];
@anonymous_array_pointed_to_by_arrayref = @{ $arrayref };

The way to read @{ $arrayref } is 'the array referred to by $arrayref'. If you want a hash, you use a similar syntax:

$hashref = 
{
    lemon      => "curd", 
    strawberry => "jam", 
    orange     => "marmalade",
};
@hashslice     = @{ $hashref }{ lemon, orange };
    # don't forget slices are plur@l data
%anonymous_hash_pointed_to_by_hashref = %{ $hashref };

This (of course?) produces yet another way of accessing the individual elements of a reference:

$hashref = 
{
    lemon      => "curd", 
    strawberry => "jam", 
    orange     => "marmalade",
};
$hashelement  = ${ $hashref }{ "lemon" };
    # the pairs inside the hash referred to by $hashref are $calar data
$arrayref     = [ 0, 1, 2, 3, 4 ];
$arrayelement = ${ $arrayref }[2];
    #the elements inside the array referred to by $arrayref are $calar too

So that if we have a dereferenced arrayref:

@{ $arrayref }

we access its elements with the usual Perl syntax:

${ $arrayref }[ INDEX ]

In simple cases, the { } braces can be omitted, although I personally never miss them off, as I get easily confused:

@array = @{ $arrayref };
@array = @$arrayref;

are equivalent nonetheless.

All the examples so far have relied on you knowing the structure of the Thingy (gungy reference mass) at compile time. Although there will be times when you will create huge perl thingies in your code (especially when you get onto object oriented programming), more often than not, you will be building them at run time from data entered by users or from other files. In fact, a great deal of programming and parsing is to do with converting data from one format to another. Data stored in flat files is called 'serialised' data. Data stored within a script will usually be more tree-like. Conversion from serialised input (such as XML files, text files or user input) to serialised output (such as an HTML file, or an email) via an internal parse tree of some sort is often a job for modules, but you'll frequently need to create a quick and dirty converter for a simple format such as the one below. Here, we use simple array and hash manipulation operators (like push, for, and hash assignment) to create a Thingy on the fly. Since references are just pointers to hash and array structures, building and manipulating them is a simple matter of getting up close and personal to these functions.

#!/usr/bin/perl
use strict;
use warnings;
my $record;
my @people;
while ( <DATA> )
{
    chomp;
    if ( /\*\*\*/ )   # start of record marker
    {
        $record = {}; # create an empty hashref
    }
    elsif ( my ( $field, $data ) = / (\w+) \s* = \s* (.*) /x )
    {
        if ( $field eq "pets" )
        {
            $data = [ split /\s*,\s*/, $data ];
            # create an anonymous arrayref:
            # you can wrap things that return lists in []
            # and create arrayrefs as simply as this. Good, isn't it?
        }
        $record->{ $field } =  $data;
        # add key/value pair to the anonymous hashref $record
    }
    elsif ( /---/ ) # end of record marker
    {
        push @people, $record; # add the hashref to the @people array
    }
    else
    {
        next;
    }
}
# we have now created a tree in memory, looking something like:
# @people =
# (
#    { name=>'andy', age=>37, pets=>[] }, 
#    { name=>'alex', age=>23, pets=>[ 'dog' ] },
#    { name=>'steve',age=>26, pets=>[ 'millipede', 'snake' ] },
# );
for my $person ( @people )
{
    print ucfirst "$person->{name} is $person->{age} years old ";
    # ucfirst capitalises the first letter of a string
    if ( my @pets = @{ $person->{pets} } )
        # assignment is neatest here, as we use @pets in a minutes
    {
        local $" = ', ';
        # the $" is a special perl variable, containing 
        # the thing used to separate array elements in quoted strings:
        # usually it's a space, but we make it a comma and space for
        # our output here
        print "and has the following pets: @pets.\n";
    }
    else
    {
        print "and has no pets.\n";
    }
}
# everything after the __DATA__ token is available to a perl script and
# can be read automagically via the DATA filehandle, which is opened
# automatically on running your script
__DATA__
***
name = andy
age = 37
pets =
---
***
name = alex
age = 23
pets = dog
---
***
name = steve
age = 26
pets = millipede, snake
---
Andy is 37 years old and has no pets.
Alex is 23 years old and has the following pets: dog.
Steve is 26 years old and has the following pets: millipede, snake.

So we take one serial format (our __DATA__ section), convert it into an internal thingy, then dump the thingy in our own chosen format.

What other practical uses are there for references? Well, if you've been playing with Perl like a good hacker, and tried to return arrays from subroutines, you'll find that it doesn't work how you expected:

#!/usr/bin/perl
use strict;
use warnings;
my ( @one, @two ) = thingy();
print "one @one\ntwo @two\n";
sub thingy
{
    my @first  = qw/ lemon orange lime/;
    my @second = qw/ apple pear medlar/;
    return @first, @second;
}
one lemon orange lime apple pear medlar
two

The problem is that arrays in list context (such that return gives its arguments) interpolate their members. That is, return flattens @first and @second into a single, big list. The second problem is that arrays are greedy, so when we return this flattened list from the subroutine, @one slurps up all the return values, and @two gets nothing. There is a kludge to get around this:

my ( @one, @two );
( @one[ 0 .. 2 ], @two[ 0 .. 2 ] ) = thingy();

but this is obviously dependent on knowing how many elements are returned: this will break if the two arrays have variable lengths. What we really need to do is pass the arrays by reference:

#!/usr/bin/perl
use strict;
use warnings;
my ( $one, $two ) = thingy();
print "one @{$one}\ntwo @{$two}\n";
sub thingy
{
    my @first  = qw/ lemon orange lime/;
    my @second = qw/ apple pear medlar/;
    return \@first, \@second;
}

The subroutine returns a list of arrayrefs (effectively an array of arrays), which the body of the program then plays with using the @{ $ref } dereferencing syntax. You can be explicit if you want, and dereference the arrayrefs back to real arrays if you'd rather:

my ( $one, $two ) = thingy();
my @one = @{ $one };
my @two = @{ $two };

There's something rather important to note about passing things by reference instead of by value:

#!/usr/bin/perl
use strict;
use warnings;
my @spices = qw( ginger kratchai galangal );
by_value( @spices );
print "@spices\n";
sub by_value
{
    my @spices = @_;
    push @spices, "turmeric";
}
ginger kratchai galangal

When you pass things by value, you are passing copies of the data, so manipulating these data in the sub does nothing interesting to the original data (you'd have to return the altered array, and capture it in @spices to change the original data). However, when you pass by reference, you are passing pointers to the original data, so if you manipulate what the references point to, you are modifying the original data:

#!/usr/bin/perl
use strict;
use warnings;
my @spices = qw( ginger kratchai galangal );
by_reference( \@spices );
print "@spices\n";
sub by_reference
{
    my $spices = shift;
    push @{ $spices }, "turmeric";
}
ginger kratchai galangal turmeric

Bear this in mind! Another source of subtle bugs is messing with the elements of @_:

#!/usr/bin/perl
use strict;
use warnings;
my @spices = qw( ginger kratchai galangal );
by_value( @spices );
print "@spices\n";
sub by_value
{
    $_[0] = 'cardamom';
}
cardamom kratchai galangal

Like $_ in a foreach loop, the items in @_ are aliased to the items in the list of arguments passed to the subroutine. Modifing them will modify the data in the body of the program, which is generally something you don't want to do. It's always best to be explicit if you want the subroutine to modify its arguments, pass by reference, if you don't, pass by value, and immediately copy the contents of @_ into some nice lexically scope variables.

References can get quite complicated, which is where the module Data::Dumper comes in very handy. Modules can be used, like you have seen with strict: they imports some extra functionality to your program, in this case, a function called Dumper().

#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
my @complex =
(
    "an array",
    {
        containing => "a hashref"
    },
    "a scalar",
    [
        [
            "a two deep arrayref",
            "with two elements"
        ],
        {
            lemon => "curd"
        }
    ]
);
print Dumper( \@complex );
$VAR1 =
[
  'an array',
  {
    'containing' => 'a hashref'
  },
  'a scalar',
  [
    [
      'a two deep arrayref',
      'with two elements'
    ],
    {
      'lemon' => 'curd'
    }
  ]
];

Data::Dumper takes a reference to any perl data structure and spits out the contents in a) a pretty printed format, and b) in a format that you can actually print to a file:

open my $FILE, $file;
print $FILE Dumper( \@complex );

to save it for later (this is called serialisation). You can then recover the data with:

do $file;

You may remember a while ago that I said you can create anonymous subroutines:

$coderef = sub { return 2*$_[0] };

To use these, you can dereference them like normal, using () parentheses, as you might have guessed:

print $coderef->(3);
6

This is very powerful: it means you can actually return bits of code from subroutines:

#!/usr/bin/perl
use strict;
use warnings;
my $multiply = <STDIN>;
my $coderef  = construct( $multiply );
print $coderef->( 2 );
sub construct
{
    my $number  = shift;
    my $coderef = sub { return $number * $_[0] };
    return $coderef;
}

This creates coderefs on the fly which will multiply by whatever number you decide via STDIN. The technical term for such things is closures: the weird thing about them is that the $number in sub construct is a lexically scoped my variable, and ought to disappear forever (go out of scope) when you leave the subroutine. However, when you use:

$coderef->( 2 ); # or equivalently
&{ $coderef }( 2 ); 
    # & is the sigil for subroutines, like arrays get @ and hashes get %

the value of $number you entered when you constructed the coderef is still there, deeply bound into the coderef, even though it 'shouldn't' really exist outside the scope of sub construct. This is perl magic.

So far we have only talked about so called hard references. There are also such things as soft references, which if you try to use when strict is on will barf. In the interests of hygiene, I'll not tell you about them, they're not much used, for good reason, and you can always find out about them yourself. If you ever think you need one, it's almost certain what you actually need is a hash.

Bag of tricks

There's another way of creating code on the fly, besides closures. This is eval. eval comes in two flavours: string and block. The block eval looks like:

#!/usr/bin/perl
use strict;
use warnings;
$number = <STDIN>;
my $answer;
eval
{
    $answer = 2 / $number;
};
print $@ ? "Divide by zero error or similar: $@" : $answer;

This is useful (and indeed the only way) to catch exceptions, that is, divide-by-zero errors and their ilk. Note you need a ; at the end of the eval { BLOCK }; because eval is a statement. If something goes wrong in the block, the special variable $@ is set with what went wrong. So eval BLOCK allows you to test perl code, and make fatal things non-fatal.

If you actually want to create new code on the fly, you can use eval STRING:

#!/usr/bin/perl
use strict;
use warnings;
my $name = <STDIN>;
eval "sub $name { return \"hello\" } ";

This will create a subroutine on the fly called $name that returns 'hello', which you can then call normally. The string is quoted, and the usual rules for double quoted string interpolation apply. eval is very powerful, which you should be aware of:

my $cmd = <STDIN>;
eval $cmd;

is going to get you in an awful lot of trouble if someone types in

system("rm -rf *")

on Unix, or

system("DELTREE c:\\windows")

if you're on Windows. Beware.

OK. We're getting there, in terms of having covered nearly everything but modules and classes. However, here's a random bag of other Perl tricks no-one should be without.

Firstly, telling the time:

$time = scalar( localtime );
print $time;

localtime returns the local time in an array, perldoc -f localtime for details. The commonest way of using it though, is calling it in scalar context, which returns a useful descriptive string.

Secondly, going to bed:

sleep 1;

makes perl sleep for 1 second (ish: subject to some iffiness).

Thirdly, making noises:

print "\a";

\a is an alarm beep.

Fourthly, printing things prettily. Perl has two functions for this, printf and sprintf. Both use the same syntax, but printf actually prints the prettified string, whereas sprintf just returns it, so you can store the prettified version elsewhere, e.g. in a variable. The format is a bit complex: perldoc -f sprintf for the gory details.

printf takes at least two arguments. The first is a control string, the second is the string to prettify. Control strings contain placeholders that start with %. They end with a letter that indicates the format you want: f is a fixed decimal floating point number. e is a scientific notation float, s is a vanilla string, and u is an unsigned (no + or −) integer, and so on. Between the % and the letter can come some bits and bobs to specify the format you want your string in. If you put a number in, it specifies the minimum field width. If you put a - in between, it means left justify, so:

#!/usr/bin/perl
use strict;
use warnings;
my $string1 = "carrots";
my $string2 = "beans";
printf
(
    "%-10s neatly lined up\n%-10s neatly lined up too",
     $string1,              $string2
);
carrots   neatly lined up
beans     neatly lined up too

See that the %-10s in the control string act like place-holders for the list of strings that follow. You can format your code nicely so that its readers will know which placeholder refers to which string.

Another useful one is:

#!/usr/bin/perl
use strict;
use warnings;
my $string = "1.23465326362643743657563";
printf( "%.3f", $string );
1.235

A . followed by a number indicates a maximum number of decimal places. Incidentally, if you need to print a literal % character in a control string, you'll need to escape it, like this: %%. I won't cover them here, but another way of messing with strings is using the pack and unpack operators, which allow you to convert between strings (like "100") and (for example) their binary equivalent (i.e. the actual 8-bit binary string 00000100). perldoc -f pack for the details.

Fifthly, I mentioned earlier about loop control. If you have a bunch of loops embedded in each other:

while ( <$FILE> )
{
    while ( my $word = split /\s+/, $_ )
    {
        print "$word" unless $word =~ /^do_not_print_me$/i;
    }
}

You'll often want to abort loops prematurely. For this you'll want next and last. Both drop you out of the current innermost loop: next skips any remaining code, and restarts the loops with the next value, whilst last kills the innermost loop dead. In this case next will move onto the next $word, whilst last will ignore the rest of the $words generated by split :

while ( <$FILE> )
{
    while ( my $word = split /\s+/, $_ )
    {
        next if $word =~ /^#/;
            # ignore any 'word' starting with a #
        last if $word =~ /^END_OF_LINE$/;
            # ignore the rest of the words in the line
        print $word;
    }
}

The problem with this is that maybe you want to drop out of the outer loop if you find something in the inner loop. To do this, you can use labels. For example, if you were trying to parse a Perl file (not a good idea: it is widely known that the only thing that can parse Perl code properly is the perl interpreter), you might try something like this:

LINE:
while ( <$FILE> )
{
    WORD:
    while ( my $word = split /\s+/, $_ )
    {
        next LINE if $word =~ /^#/;
            # ignore the rest of the line, it's only a comment
        last LINE if $word =~ /^__END__$/;
            # ignore the rest of the lines if you find Perl's __END__ token
        next WORD if $word =~ /rude/;
            # don't print anything rude
        print $word;
    }
}

The LABEL:s allow you to jump out of a loop from any depth, which is very useful. The much deprecated goto LABEL allows you to jump to anywhere in the code, but now I've told you about it, forget about it, there is a whole world of hurt there. There's also redo which makes for the simplest loop possible:

LOOP:
{
    print "Hello. Did you know you can kill a perl program with Control-C\n";
    redo LOOP;
}

This sort of loop seems to be looked down upon, but I've found it useful from time to time.

Summary

References can be made to real variables, or created directly and anonymously:

# named arrayref
@array     = ( qw/q w e rt y/ );
$array_ref = \@array;
# anonymous hashref
$hash_ref  = { key => "value", lemon => "curd" };
#anonymous coderef
$code_ref  = sub { return 2**$_[0] };

References can be dereferenced by chasing pointers:

#!/usr/bin/perl
use strict;
use warnings;
my $thingy =
{
    name =>
    {
        fore => "Steve",
        sur  => "Cook"
    },
    age => sub { (localtime)[5] + 1900 - 1977 }
};
print $thingy->{name}->{fore}, $thingy->{age}->();
    # or equivalently
print $thingy->{name}{fore}, $thingy->{age}();

Or accessed using the block syntax

#!/usr/bin/perl
use strict;
use warnings;
my $thingy =
{
    name =>
    {
        fore => "Steve",
        sur  => "Cook"
    },
    age  => sub { (localtime)[5] + 1900 - 1977 }
};
print values %{ $thingy->{name} };

They can be used to pass data by reference to subroutines. eval can be used to trap exceptions (like divide-by-zero errors), (s)printf can be used for formatted printing (as can format and write, but you don't want to go there, and I'm not sure if anyone's gone there in years!), and labels, next and last can be used for loop control.

Test yourself

See if you can write a script that does the following:

#!/usr/bin/perl
use strict;
use warnings;
my $elements =
[
    { name => 'neutron',   symbol => 'n'  },
    { name => 'hydrogen',  symbol => 'H'  },
    { name => 'helium',    symbol => 'He' },
    { name => 'lithium',   symbol => 'Li' },
    { name => 'beryllium', symbol => 'Be' },
    { name => 'boron',     symbol => 'B'  },
    { name => 'carbon',    symbol => 'C'  },
    { name => 'nitrogen',  symbol => 'N'  },
    { name => 'oxygen',    symbol => 'O'  },
    { name => 'fluorine',  symbol => 'F'  },
    { name => 'neon',      symbol => 'Ne' },
    { name => 'sodium',    symbol => 'Na' },
    { name => 'magnesium', symbol => 'Mg' },
];
my $number = 7;
print "The name of element 7 is ", get_name_by_number( 7 ), "\n";
print "\nThe elements...\n\n";
print "$_\n" for list_elements();
sub get_name_by_number
{
    my ( $element ) = @_;
    $elements->[ $element ]->{name};
}
sub list_elements
{
    my @pretty;
    for ( @{ $elements } )
    {
        push @pretty, sprintf "%3s $_->{name}", $_->{symbol};
    }
    return @pretty;
}

This'll be the last test yourself section. From now on, if you need encouragement to use Perl, then you're probably beyond help ☺

Next…