http://www.perlmonks.org?node_id=189267

BorgCopyeditor has asked for the wisdom of the Perl Monks concerning the following question:

Yesterday, I finally figured out what a closure was, at the same time that I figured out that I might have a use for one. I have a subroutine that encodes a (usually long) list of words into a given character-encoding scheme that depends on a user's preference.

Currently my subroutine stupidly checks what the user's configuration was set to for each word. I thought it would be more reasonable to use the configuration option to produce the appropriate subroutine once and for all at the beginning of the program, like so:

$encode = 'u'; # example, could be 'p', 'b', or 'u' $sub_ref = template($encode); sub template { my $e = shift; if ($e eq 'b') { return sub {...stuff...} } elsif ($e eq 'u') { return sub {...other stuff...} } else { return sub {...yet more...} } } ...time passes... $encoded_output = &$sub_ref;

The problem I'm facing now is that "stuff", "other stuff", and "yet more" share a fair number of common elements. My question is whether there is some simple way to build up the pieces of an anonymous subroutine, such that they could be assembled at the right time.

My current approach will probably be something like this:

sub template { my $e = shift; my $common = sub {...shared stuff...} if ($e eq 'b') { return sub {&$common; ...stuff...} } if ($e eq 'u') { return sub {&$common: ...other stuff...} } etc.

This way, I don't have to type the common pieces of code (and there are a few) into each of the cases. Does this seem like a reasonable way to "build" the pieces of such a function? Are there other ways to do this that make mine look like "baby Perl"?

BCE
--RezistenceResistance is futile.

Title edit by tye

Replies are listed 'Best First'.
Re: Closures
by Aristotle (Chancellor) on Aug 11, 2002 at 04:53 UTC
    How about:
    sub template { my $e = shift; my $not_common_a = $e eq 'b' ? sub { ... } : $e eq 'u' ? sub { ... } : sub { ... } ; my $not_common_b = $e eq 'x' ? sub { ... } : sub { ... } ; return sub { $common_stuff; $not_common_a->(); $more_common_stuff; $not_common_b->(); } }
    ____________
    Makeshifts last the longest.

      Thanks. It looks obvious ... now that I've seen a solution. :) I ended up doing it inside out, as it were, initially defining an anonymous sub just for the part of the enclosing sub that needed differing behaviors, and calling that within that enclosing sub.

      The conditional assignment thingy with nested ternary '?'s is very elegant, I think.

      Thanks again,

      BCE

Re: Building an anonymous subroutine
by Basilides (Friar) on Aug 12, 2002 at 10:07 UTC
    Perhaps you could put your common code in strings and use eval statements to reference it:
    $encode = 'p'; $sub_ref = test($encode); sub test { my $e = shift; my $eval1 = 'print "first bit of reusable code\n";'; my $eval2 = 'print "second bit of reusable code\n";'; $e eq 'p'? return sub { eval $eval1; print "you typed 'p' as your option\n"; eval $eval2; }: $e eq 'b'? return sub { eval $eval1; print "you typed 'b' as your option\n"; eval $eval2; }: return sub { eval $eval1; print "you typed 'u' as your option\n"; eval $eval2; }; } $encoded_output = &$sub_ref;
    Sorry this is such a cruddy example!

      Instead of returning subs which constantly evals strings (your eval $evalN lines), you can just eval a string that defines a sub once, something like this:

      #!/usr/bin/perl -l use strict; use warnings; my @subs = ( q/ { my $i=0; sub { print "($i) First sub, sir!"; ++$i; } } /, q/ { my $i=0; sub { print "($i) Another sub, sir!"; ++$i; } } / ); my @code = map { eval $_ } @subs; $code[rand(2)]->() for (0 .. 20);

      I hope that isn't too obscure. It just places two strings into an array. Each string is a block of code defining a variable and returning a sub using that variable. Later the strings are eval'ed in order to generate callable code. And last, the code is actually called. The random index is there just to make it clear that there is a closure in the game.

      Sorry about that "sir!" thing, I saw Full Metal Jacket the other day and I can't get it out of my head. Be happy that my subs aren't mooing.

      Doing an eval without checking $@ and throwing a useful error message is a really bad idea. I would also suggest reading the section on Plain Old Comments in perlsyn and using that to cause any errors thrown from within the subroutine to have more useful and meaningful names than just eval 38.
Re: Building an anonymous subroutine
by oylee (Pilgrim) on Aug 12, 2002 at 19:45 UTC
    Perhaps a hash of subs would be useful to lift out the conditional tests also, e.g.,
    sub template { my $e = shift; my %actionhandlers = ( p => sub {...blougat...}, b => sub {...blimblim...}, u => sub {...hoohaa...} ... ); return sub { ... common stuff ... $actionhandlers{$e}->(...args_if_any...); ... more stuff ... }; }

    Anonymous Lee
Re: Building an anonymous subroutine
by thunders (Priest) on Aug 12, 2002 at 11:06 UTC
    My first thought would be to either put common code into another sub (or series of subs). Or altenatively to execute the uncommon code conditionally with if or unless blocks.
Re: Building an anonymous subroutine
by stephen (Priest) on Aug 13, 2002 at 03:22 UTC

    Sometimes a closure is an object waiting to happen. (And sometimes the reverse.) Consider:

    package Encoder; ## Basic constructor sub new { my $type = shift; my $self = {}; bless $self, $type; } ## Define the common interface sub shared_stuff { return reverse $_[1]; } ## Define the uncommon interface sub other_stuff { die "Somebody didn't define the other stuff!"; } ## Generalized encoding method sub encode { my $self = shift; my ($text) = @_; $self->common_stuff($text); $self->other_stuff($text); }
    That defines your base class, where all the common code can live.

    Then, for each kind of encoder, you can define a subclass. For many subclasses, they'll be doing the same overall thing with variations, and you can just redefine the 'other_stuff' method. Otherwise, you can redefine 'encode' itself... but you'll still have 'common_stuff' to call if you want it.

    # In Encoder/B.pm package Encoder::B; use base 'Encoder'; sub other_stuff { my ($text) = @_; my $new_text = $text; $new_text =~ s/a/c/g; }

    Then, finally, you only need a factory subroutine to create the right type of encoder for each user preference:

    use Encoder::B; use Encoder::Whatever; our %Encoder_Table => ( 'b' => 'Encoder::B', 'c' => 'Encoder::Whatever', ); sub encoder_factory { my ($e) = @_; exists $Encoder_Table{$e} or die "'$e' is not a valid type of encoder, sorry!"; return $Encoder_Table{$e}->new(); }

    And, finally, to get your new encoder and use it:

    my $encoder = encoder_factory($whatever_e_is); print $encoder->encode("Hahahaha!");
    There will be a speed hit, but it'll be faster than doing constant per-word checks, and easier to keep organized than closures-upon-closures-upon-closures.

    Note: Code is untested and I've been drinking Cabernet.

    stephen

      Closures upon closures is not intrinsically easier or harder to keep organized than subclasses upon subclasses.

      They just organize naturally in different ways. If a clean OO model fits your problem, then OO naturally channels you. If it doesn't, then using closures is better than trying to fight OO into an imitation of what you would do more naturally with closures. Particularly so in Perl where writing any OO involves so much infrastructure.

      BTW your extra method-call hooks could be replaced with a single method that is intended to be called from subclasses with SUPER. Less code and infrastructure, same result.

Re: Building an anonymous subroutine
by BorgCopyeditor (Friar) on Aug 13, 2002 at 14:20 UTC

    Thanks to everyone who contributed to this thread. Wow. There truly is more than one way to do it! We've got nested anonymous subs, code strings mapped with eval, a quasi-switch statement disguised as a hash, an OO solution ... I've learned a bunch here.

    Thanks again, all,

    BCE
    --You'reYour punctuation skills are insufficient!