Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

Re: Ways to implement a closure

by tmoertel (Chaplain)
on Oct 15, 2004 at 21:07 UTC ( [id://399664]=note: print w/replies, xml ) Need Help??


in reply to Ways to implement a closure

(Updated 20041017: Added final, put-it-all-togther example.)

While closures are almost always created via anonymous subroutines (a la sub { ... }) at the lowest level, the real fun only comes about when you delegate this chore to some kind helper function that builds anonymous subroutines for you. Thus, in heavily functional code, you usually don't use sub { } yourself but instead rely upon a library of helpers to do the grunt work for you.

For example, here's a function that builds functions to append a given suffix to strings:

sub make_suffix_appender { my $suffix = shift; return sub { return $_[0] . $suffix; } }
It takes a given suffix, remembers it, and returns a new function that will apply the remembered suffix to any string we give it:
my $bang_appender = make_suffix_appender("!"); print $bang_appender->("Hello"); # Hello! print $bang_appender->("Dude"); # Dude!
We can have even more fun if we create higher-order functions that remember other functions in their closures. Here's a pair of functions that can be used to apply other functions to a list of arguments as a whole or to each argument, one at a time:
sub apply { my $fn = shift; return sub { $fn->(@_); } } sub apply_to_each { my $fn = shift; return sub { map { $fn->($_) } @_; } }
We might use them like so:
my $upcase = apply( sub { uc("@_") } ); print $upcase->("Hello"); # HELLO print $upcase->("Hello", "world"); # HELLO WORLD my $parenify_all = apply( sub { "(@_)" } ); my $parenify_each = apply_to_each( $parenify_all ); print $parenify_all ->("Hello", "world"); # (Hello world) print $parenify_each->("Hello", "world"); # (Hello)(world)
We can also capture regex values in closures to good effect:
sub make_regex_matcher { my $regex = shift; return sub { local $_ = $_[0]; /$regex/g; } }
Now we can use the helper function to build "regex matchers":
my $digit_matcher = make_regex_matcher( qr/(\d+)/ ); my @digits = $digit_matcher->( "123-blah-45-6789" ); print "@digits"; # 123 45 6789 my $word_matcher = make_regex_matcher( qr/(\w+)/ ); my @words = $word_matcher->( "123-blah-45-6789" ); print "@words"; # 123 blah 45 6789
Why would we want to make all these little functions? Because we can glue them together to make complicated things happen.

One of the most fundamental kinds of glue is composition, which turns individual functions into function pipelines:

sub compose2 { my ($f, $g) = @_; return sub { $f->( $g->( @_ ) ); } }; use List::Util qw( reduce ); sub compose { no warnings qw( once ); reduce { compose2($a,$b) } @_; }
Now let's glue some of our earlier functions together to make some simple pipelines:
my $up_banger = compose($bang_appender, $upcase); print $up_banger->("Hello"); # HELLO! my $upcase_words = compose($upcase, $word_matcher); print $upcase_words->( "123-blah-45-6789" ); # 123 BLAH 45 6789 my $parenify_words = compose($parenify_each, $word_matcher); print $parenify_words->( "123-blah-45-6789" ); # (123)(blah)(45)(6789)
Also, note that we don't need to store our pipelines in a variable before we use them. We can call them on the fly:
print compose($parenify_each, $word_matcher)->("a b c"); # (a)(b)(c)
Note that when building pipelines using compose, the input data flows through the functions right-to-left (following mathematical convention). Watch what happens if we parenify first and then match for words:
print compose($word_matcher, $parenify_each)->("a b c"); # abc
As a final example, let's build 27 different pipeline combinations from our earlier collection of functions and see what each pipeline does when applied to a reference string. To help us out, let's first build id – the identity function – which simply returns its input as its output. We'll use it to pass data through stages of our pipeline unchanged.
my $id = sub { wantarray ? @_ : $_[0] };
And now, our pipeline playground:
my $fs = 0; for my $f ($id, $parenify_all, $parenify_each) { my $fd = (qw( id- pAl pEa ))[$fs++]; my $gs = 0; for my $g ($id, $word_matcher, $digit_matcher) { my $gd = (qw( id- wrd dig ))[$gs++]; my $hs = 0; for my $h ($id, $upcase, $bang_appender) { my $hd = (qw( id- up- bng ))[$hs++]; print "$fd-$gd-$hd: ", compose($f,$g,$h)->("Eat more 3.14"); } } }
Here's the output:
id--id--id-: Eat more 3.14 id--id--up-: EAT MORE 3.14 id--id--bng: Eat more 3.14! id--wrd-id-: Eatmore314 id--wrd-up-: EATMORE314 id--wrd-bng: Eatmore314 id--dig-id-: 314 id--dig-up-: 314 id--dig-bng: 314 pAl-id--id-: (Eat more 3.14) pAl-id--up-: (EAT MORE 3.14) pAl-id--bng: (Eat more 3.14!) pAl-wrd-id-: (Eat more 3 14) pAl-wrd-up-: (EAT MORE 3 14) pAl-wrd-bng: (Eat more 3 14) pAl-dig-id-: (3 14) pAl-dig-up-: (3 14) pAl-dig-bng: (3 14) pEa-id--id-: (Eat more 3.14) pEa-id--up-: (EAT MORE 3.14) pEa-id--bng: (Eat more 3.14!) pEa-wrd-id-: (Eat)(more)(3)(14) pEa-wrd-up-: (EAT)(MORE)(3)(14) pEa-wrd-bng: (Eat)(more)(3)(14) pEa-dig-id-: (3)(14) pEa-dig-up-: (3)(14) pEa-dig-bng: (3)(14)
I hope this gives you some idea of how you can create and benefit from closures without directly having to create anonymous subroutines. Just create helper functions to do the work for you.

Cheers,
Tom

P.S. For a more practical fun-with-closures example, see Re: Redirecting STDOUT from internal function with 5.6.1 restrictions, which shows how to capture output from a filehandle (such as STDOUT or STDERR) temporarily.

Replies are listed 'Best First'.
Shameless praise
by pernod (Chaplain) on Oct 16, 2004 at 18:09 UTC

    tmoertel, your functional programming tutorials in Perl continuosly boggles my mind. If you keep up like this I'll probably end up learning all I need on functional programming before I ever get around to learning Haskell.

    Thank you very much!

    fanboy pernod
    --
    Mischief. Mayhem. Soap.

Re^2: Ways to implement a closure
by tos (Deacon) on Oct 18, 2004 at 08:51 UTC
    Great stuff. This really broadens my perl-horizon.

    Thanks, tos


    Is simplicity best or simply the easiest Martin L. Gore

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://399664]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others studying the Monastery: (8)
As of 2024-03-28 12:28 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found