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:
When run, this outputs: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";
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.
#!/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
See also: Closure on Closures, How A Function Becomes Higher Order, Currying--useful examples?
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: RFC: Practical closure example: Spreadsheet writer
by GrandFather (Saint) on Aug 04, 2007 at 23:52 UTC | |
by clinton (Priest) on Aug 05, 2007 at 00:00 UTC | |
by chromatic (Archbishop) on Aug 05, 2007 at 00:41 UTC | |
|
Re: RFC: Practical closure example: Spreadsheet writer
by hilitai (Monk) on Aug 04, 2007 at 20:23 UTC | |
|
Re: RFC: Practical closure example: Spreadsheet writer
by jmcnamara (Monsignor) on Aug 06, 2007 at 10:43 UTC | |
|
Re: RFC: Practical closure example: Spreadsheet writer
by smahesh (Pilgrim) on Aug 06, 2007 at 03:45 UTC |