Closures take a while to figure out, and most examples that are given are quite abstract, making it difficult to understand just how useful a concept they are. I recently used a closure as a neat way to write data to a spreadsheet, so I thought I'd present it here as an explanation.

First, what is a closure?

perlref explains a closure as follows:

Closure is a notion out of the Lisp world that says if you define an anonymous function in a particular lexical context, it pretends to run in that context even when it's called outside the context. In human terms, it's a funny way of passing arguments to a subroutine when you define it as well as when you call it. It's useful for setting up little bits of code to run later, such as callbacks.
So, a simple example would be this:
sub make_fruit_counter { my $fruit = shift; my $total = 0; return sub { my $number = shift; $total = $total + $number; return "$total $fruit".($total > 1 ? 's' : ''); }; } my $apple = make_fruit_counter('apple'); my $orange = make_fruit_counter('orange'); print $apple->(1)."\n"; print $apple->(3)."\n"; print $orange->(1)."\n"; print $apple->(5)."\n"; print $orange->(1)."\n";
When run, this outputs:
1 apple 4 apples 1 orange 9 apples 2 oranges

The sub make_fruit_counter returns a closure, which uses lexical variables ($fruit and $number) which have been declared outside the anonymous subroutine. So the closure has its own PRIVATE copies of these variables and remembers them. This is why, when you increase the total number of apples ( $total = $total + $number), it increases only the $total variable related to the closure stored in $apples.

But this example isn't very useful, so back to the spreadsheet.

The typical process of creating a simple spreadsheet (and setting the column widths wide enough to display each column in full) involves these steps:

#!/usr/bin/perl use strict; use warnings; use Spreadsheet::WriteExcel(); my $book = Spreadsheet::WriteExcel->new('example.xls') or die "Couldn't create spreadsheet : $!"; my $sheet = $book->add_worksheet('Example sheet'); ## Create the title format 'bold' my $bold = $book->add_format(); $bold->set_bold(); my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); my @max_col_width; my $row = my $col = 0; ## Write titles $sheet->write( $row, $col++, $_, $bold ) for ('Year',@months); ## Move to beginning of next row $col = 0; $row++; ## Write arbitrary values my $val = 0; for my $year ( 2003 .. 2007 ) { ## Decide whether to increase the max width for the year colum +n my $width = length($val); $max_col_width[$col] = $width if !defined $max_col_width[$col] || $width > $max_col_width[$col]; $sheet->write($row,$col++,$year); for ( 1..@months) { ## Decide whether to increase the max width for this colum +n my $width = length($val); $max_col_width[$col] = $width if !defined $max_col_width[$col] || $width > $max_col_width[$col]; $sheet->write( $row, $col++, $val ); $val = $val + 50; } ## Move to the beginning of the next row $row++; $col = 0; } ## Set the width of each column to the width of the longest value foreach my $column (0..$#max_col_width) { $sheet->set_column($column,$column,$max_col_width[$column]); } $book->close or die "Couldn't close spreadsheet : $!";

This is fine if all you want to do is to write one spreadsheet. But for the next spreadsheet you want to write, you need to use a lot of the same code. Or maybe you want to write the same data to two different sheets, but on one sheet, you need to have an extra column on each row before the other columns. It quickly becomes a maintenance nightmare.

Really, all you want to be able to do is:

In this solution, I use the sub make_writer($sheet) to create a closure which handles these three functions (write, next_row, and set_col_widths):

#!/usr/bin/perl use strict; use warnings; use Spreadsheet::WriteExcel(); my $book = Spreadsheet::WriteExcel->new('example.xls') or die "Couldn't create spreadsheet : $!"; my $bold = $book->add_format(); $bold->set_bold(); ## Create an anonymous sub for writing to a new worksheet my $writer = make_writer($book->add_worksheet('Example sheet')); my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); $writer->( 'write', $_, $bold ) for ('Year',@months); $writer->('next_row'); my $val = 0; for my $year (2003..2007 ) { $writer->('write',$year); for (1..@months) { $writer->( 'write', $val ); $val = $val + 50; } $writer->('next_row'); } $writer->('set_col_widths'); $book->close or die "Couldn't close spreadsheet : $!"; #=================================== sub make_writer { #=================================== ## These lexical variables will be remembered inside the anony +mous sub my $sheet = shift; my $col = 0; my $row = 0; my @max_col_width; return sub { my $action = shift; ## Write to the current cell and move to the next cell if ( $action eq 'write' ) { my ( $value, $format ) = @_; ## Set the max_col_width my $width = length($value); $max_col_width[$col] = $width if !defined $max_col_width[$col] || $width > $max_col_width[$col]; $sheet->write( $row, $col, $value, $format ); return $col++; } ## Move to the beginning of the next row elsif ( $action eq 'next_row' ) { $row++; my $orig_col = $col; $col = 0; return $orig_col; } ## Set the column widths to the width of the widest value elsif ( $action eq 'set_col_widths' ) { for ( my $col = 0; $col < @max_col_width; $col++ ) { $sheet->set_column( $col, $col, $max_col_width[$co +l] ); } return; } $action ||= ''; die "Unknown action '$action'"; }; }

While, in this case, the total code is longer, the loop for writing a spreadsheet is a lot shorter and more readable, and thus easier to maintain.

If, for instance, we wanted to do a separate sheet for each deparment, and a combined sheet with the same data for the whole company, the main loop would look like this:

#!/usr/bin/perl use strict; use warnings; use Spreadsheet::WriteExcel(); my $book = Spreadsheet::WriteExcel->new('example.xls') or die "Couldn't create spreadsheet : $!"; my $bold = $book->add_format(); $bold->set_bold(); my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); ## Write titles for the company spreadsheet my $company = make_writer($book->add_worksheet('Company')); $company->('write',$_,$bold) for ('Department','Year',@months); $company->('next_row'); my $val = 0; for my $dept_name qw (North South West East) { ## Create a new worksheet for this department and add titles my $dept = make_writer($book->add_worksheet($dept_name)); $dept->('write',$_,$bold) for ('Year',@months); $dept->('next_row'); for my $year (2003..2007 ) { # The company sheet has the extra column "Department" $company->('write',$dept_name); $company->('write',$year); $dept->('write',$year); for (1..@months) { $dept->( 'write', $val ); $company->( 'write', $val ); $val = $val + 50; } $dept->('next_row'); $company->('next_row'); } $dept->('set_col_widths'); } $company->('set_col_widths'); $book->close or die "Couldn't close spreadsheet : $!";

With this solution, we don't need to keep track of the current row and column separately for each sheet, as this is all wrapped up in the closure

Note: I could have used a dispatch table rather than using the if/elsif in my anonymous sub, but I opted against it because

  1. I would need to pass too many variables around, making it more complicated than it needs to be
  2. The if options are listed in order of frequency, so the most commonly used option (write) is hit on the first if

See also: Closure on Closures, How A Function Becomes Higher Order, Currying--useful examples?


In reply to RFC: Practical closure example: Spreadsheet writer by clinton

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post, it's "PerlMonks-approved HTML":



  • Posts are HTML formatted. Put <p> </p> tags around your paragraphs. Put <code> </code> tags around your code and data!
  • Titles consisting of a single word are discouraged, and in most cases are disallowed outright.
  • Read Where should I post X? if you're not absolutely sure you're posting in the right place.
  • Please read these before you post! —
  • Posts may use any of the Perl Monks Approved HTML tags:
    a, abbr, b, big, blockquote, br, caption, center, col, colgroup, dd, del, details, div, dl, dt, em, font, h1, h2, h3, h4, h5, h6, hr, i, ins, li, ol, p, pre, readmore, small, span, spoiler, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, tt, u, ul, wbr
  • You may need to use entities for some characters, as follows. (Exception: Within code tags, you can put the characters literally.)
            For:     Use:
    & &amp;
    < &lt;
    > &gt;
    [ &#91;
    ] &#93;
  • Link using PerlMonks shortcuts! What shortcuts can I use for linking?
  • See Writeup Formatting Tips and other pages linked from there for more info.