Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Beginner's Introduction to Perl 5.10, Part 2
by chromatic, Doug Sheppard | Pages: 1, 2, 3

Subs

So far, the example Perl programs have been a bunch of statements in series. This is okay if you're writing very small programs, but as your needs grow, you'll find it limiting. This is why most modern programming languages allow you to define your own functions; in Perl, we call them subs.

A sub, declared with the sub keyword, adds a new function to your program's capabilities. When you want to use this new function, you call it by name. For instance, here's a short definition of a sub called boo:

use 5.010;

sub boo {
    say 'Boo!';
}

boo();   # Eek!

Subs are useful because they allow you to break your program into small, reusable chunks. If you need to analyze a string in four different places in your program, it's much easier to write one analyze_string sub and call it four times. This way, when you make an improvement to your string-analysis routine, you'll only need to do it in one place, instead of four.

In the same way that Perl's built-in functions can take parameters and can return values, your subs can, too. Whenever you call a sub, any parameters you pass to it appear in the special array @_. You can also return a single value or a list by using the return keyword.

use 5.010;

sub multiply {
    my (@ops) = @_;
    return $ops[0] * $ops[1];
}

for my $i (1 .. 10) {
     say "$i squared is ", multiply($i, $i);
}

There's an interesting benefit from using the the my keyword in multiply? It indicates that the variables are private to that sub, so that any existing value for the @ops array used elsewhere in our program won't get overwritten. This means that you'll evade a whole class of hard-to-trace bugs in your programs. You don't have to use my, but you also don't have to avoid smashing your thumb when you're hammering nails into a board. They're both just good ideas.

You can also assign to multiple lexical variables (declared with my) in a single statement. You can change the code within multiply to something like this without having to modify any other code:

sub multiply {
    my ($left, $right) = @_;
    return $left * $right;
}

If you don't expressly use the return statement, the sub returns the result of the last statement. This implicit return value can sometimes be useful, but it does reduce your program's readability. Remember that you'll read your code many more times than you write it!

Putting it all together

The previous article demonstrated a simple interest calculator. You can make it more interesting by writing the interest table to a file instead of to the screen. Another change is to break the code into subs to make it easier to read and maintain.

[Download this program]

#! perl

# compound_interest_file.pl - the miracle of compound interest, part 2

use 5.010;

use strict;
use warnings;

# First, we'll set up the variables we want to use.
my $outfile   = 'interest.txt';    # This is the filename of our report.
my $nest_egg  = 10000;             # $nest_egg is our starting amount
my $year      = 2008;              # This is the starting year for our table.
my $duration  = 10;                # How many years are we saving up?
my $apr       = 9.5;               # This is our annual percentage rate.

my $report_fh = open_report( $outfile );
print_headers(   $report_fh );
interest_report( $report_fh, $nest_egg, $year, $duration, $apr );
report_footer(   $report_fh, $nest_egg, $duration, $apr );

sub open_report {
    my ($outfile) = @_;
    open my $report, '>', $outfile or die "Can't open '$outfile': $!";
    return $report;
}

sub print_headers {
    my ($report_fh) = @_;

    # Print the headers for our report.
    say $report_fh "Year\tBalance\tInterest\tNew balance";
}

sub calculate_interest {
    # Given a nest egg and an APR, how much interest do we collect?
    my ( $nest_egg, $apr ) = @_;

    return int( ( $apr / 100 ) * $nest_egg * 100 ) / 100;
}

sub interest_report {
    # Get our parameters.  Note that these variables won't clobber the
    # global variables with the same name.
    my ( $report_fh, $nest_egg, $year, $duration, $apr ) = @_;

    # Calculate interest for each year.
    for my $i ( 1 .. $duration ) {
        my $interest = calculate_interest( $nest_egg, $apr );
        my $line     =
            join "\t", $year + $i, $nest_egg, $interest, $nest_egg + $interest;

        say $report_fh $line;

        $nest_egg += $interest;
    }
}

sub report_footer {
    my ($report_fh, $nest_egg, $duration, $apr) = @_;

    say $report_fh "\n Our original assumptions:";
    say $report_fh "   Nest egg: $nest_egg";
    say $report_fh "   Number of years: $duration";
    say $report_fh "   Interest rate: $apr";
}

Notice how much clearer the program logic becomes when you break it down into subs. One nice quality of a program written as small, well-named subs is that it almost becomes self-documenting. Consider these four lines:

my $report_fh = open_report( $outfile );
print_headers(   $report_fh );
interest_report( $report_fh, $nest_egg, $year, $duration, $apr );
report_footer(   $report_fh, $nest_egg, $duration, $apr );

Code like this is invaluable when you come back to it six months later and need to figure out what it does -- would you rather spend your time reading the entire program trying to figure it out or read four lines that tell you the program 1) opens a report file, 2) prints some headers, 3) generates an interest report, and 4) prints a report footer?

Play around!

This article has explored files (filehandles, open(), close(), and <>), string manipulation (substr(), split() and join()) and subs. Here's a pair of exercises -- again, one simple and one complex:

  • You have a file called dictionary.txt that contains dictionary definitions, one per line, in the format "word space definition". (Here's a sample.) Write a program that will look up a word from the command line. (Hints: @ARGV is a special array that contains your command line arguments and you'll need to use the three-argument form of split().) Try to enhance it so that your dictionary can also contain words with multiple definitions in the format "word space definition:alternate definition:alternate definition, etc...".
  • Write an analyzer for your Apache logs. You can find a brief description of the common log format at http://www.w3.org/Daemon/User/Config/Logging.html. Your analyzer should count the total number of requests for each URL, the total number of results for each status code and the total number of bytes output.

Happy programming!