Hack 89. Invoke Functions in Odd Ways


Hide function calls behind familiar syntax.

Everyone's familiar with the normal ways to invoke Perl functions: foo( ) or foo(someparams...) or, in the case of method calls, class->foo(...) or $obj->foo(...).

The Hack

There are far stranger ways to invoke a function. All of these ways are usually too weird for normal use, but useful only on occasion. That's another way[4] to say that they are the perfect hack.

[4] "One of God's own prototypes. Some kind of high-powered mutant never even considered for mass production. Too weird to live, and too rare to die." Hunter S. Thompson

Make a Bareword invoke a function

If you have a function named foo, Perl will let you call it without parens as long as it has seen the function defined (or predefined) by the time it sees the call. That is, if you have:

sub foo {     ...a bunch of code.... }

or even just:

sub foo;   # predefine 'foo'

then later in your code you are free to write foo($x,$y,$z) as foo $x,$y,$z. A degenerate case of this is that if you have defined foo as taking no parameters, with the prototype syntax,[5] like so:

[5] See "Prototypes" in the Perldoc perlsub.

sub foo ( ) {     ...a bunch of code... }

or:

sub foo ( );

then you can write foo( ) as just plain foo! Incidentally, the constant pragma prior to Perl 5.9.3 defines constants this way. The Perl time( ) function (very non-constant) also uses this approachthat's why either of these syntaxes mean the same thing:

my $x = time; my $x = time( );

You could implement a function that returns time( ) except as a figure in days instead of in seconds:

sub time_days ( ) {     return time( ) / (24 * 60 * 60); } my $xd = time_days;

If you tried calling time_days, without parens, before you defined the function, you would get an error message Bareword "time_days" not allowed while "strict subs" in use. That's assuming you're running under "use strict," which of course you are.

A further example is:

use Data::Format 'time2str';   sub ymd_now ()   {       time2str( '%Y-%m-%d', time )   }   print "It is now ", ymd_now, "!!\\n";

Tie a scalar variable to a function

Perl provides a mechanism, called "tying", to call a function when someone apparently accesses a particular variable. See perldoc perltie for the agonizing details.

Consider a scalar variable whose value is really variablea variable that, when read, returns the current time, somewhat in the style of some old BASIC dialects' $TIME (or TIME$) variable:

{     package TimeVar_YMDhms;     use Tie::Scalar ( );     use base 'Tie::StdScalar';     use Date::Format 'time2str';     sub FETCH { time2str('%Y-%m-%dT%H:%M:%S', time) } } tie my $TIME, TimeVar_YMDhms; print "It is now $TIME\\n"; sleep 3; print "It is now $TIME\\n";

That produces output like:

It is now 2006-02-03T16:04:17 It is now 2006-02-03T16:04:20

You can even rewrite that to use a general-purpose class, which will produce the same output:

{     package Tie::ScalarFnParams;     sub TIESCALAR     {         my($class, $fn, @params) = @_;         return bless sub { $fn->(@params) }, $class;     }     sub FETCH { return shift( )->( ) }     sub STORE { return } # called for $var = somevalue; } use Date::Format 'time2str'; tie my $TIME, Tie::ScalarFnParams,  # And now any function and optional parameter(s):    sub { time2str(shift, time) }, '%Y-%m-%dT%H:%M:%S'; print "It is now $TIME\\n"; sleep 3; print "It is now $TIME\\n";

Tie an array variable to a function

A more sophisticated approach is to tie an array to a function, so that $somearray[123] will call that function with the parameter 123. Consider, for example, the task of giving a number an English ordinal suffixthat is, taking 2 and returning "2nd," taking 12 and returning "12th," and so on. The CPAN module Lingua::EN::Numbers::Ordinate's ordinate function can do this:

use Lingua::EN::Numbers::Ordinate 'ordinate'; print ordinate(4), "!\\n";

This shows "4th!". To invoke this function on the sly, use a tied-array class:

{     package Tie::Ordinalize;     use Lingua::EN::Numbers::Ordinate 'ordinate';     use base 'Tie::Array';     sub TIEARRAY  { return bless { }, shift } # dummy obj     sub FETCH     { return ordinate( $_[1] ) }     sub FETCHSIZE { return 0 } } tie my @TH, Tie::Ordinalize; print $TH[4], "!\\n";

which, also, shows "4th!". Perl calls the required method FETCH when reading $TH[someindex] and FETCHSIZE when reading @TH in a scalar context (like $x=2+@TH). There are other methods that you can define for accessing the tied array as $TH[123] = somevalue, push(@TH,...), or any of the various other operations you can perform on a normal Perl array. The Tie::Array documentation has all the gory details.

Tying an array to something may seem like a pretty strange idea, but Mark Jason Dominus's excellent core-Perl module Tie::File [Hack #19] puts this to good use.

Tie a hash variable to a function

One of the limitations of tying an array to a function is that the index (as FETCH sees in $somearray[ index ]) obviously has to be a number. With a tied hash, the FETCH method gets a string argument ($somehash{ index }). In this case, you can use tying to make $NowAs{ str } call time2str( str ):

{     package Tie::TimeFormatty;     use Tie::Hash ( );     use base 'Tie::StdHash';     use Date::Format 'time2str';     sub FETCH { time2str($_[1], time) } } tie my %NowAs, Tie::TimeFormatty; print "It is now $NowAs{'%Y-%m-%dT%H:%M:%S'}\\n"; sleep 3; print "It is now $NowAs{'%c'}\\n";

That produces output like:

It is now 2006-02-03T18:28:06 It is now 02/03/06 18:28:09

An earlier example showed how to make a class Tie::ScalarFnParams which makes any scalar variable call any function with any parameters. You can more easily do the same thing for hashesexcept that it already exists. It's the CPAN module called Interpolation, originally by Mark Jason Dominus. Use it to rewrite the previous code like:

use Date::Format 'time2str'; use Interpolation NowAs => sub { time2str($_[0],time) }; print "It is now $NowAs{'%Y-%m-%dT%H:%M:%S'}\\n"; sleep 3; print "It is now $NowAs{'%c'}\\n";

Other hacks based on this on the CPAN include Tie::DBI, which makes $somehash{ somekey } to query arbitrary databases (or DB_File to make it query a mere Berkeley-style database) and Tie::Ispell which makes $dict{ word } spellcheck the word and suggest possibilities if it appears incorrect.

Of course, you can also tie a filehandle to a function [Hack #90].

Add a function-calling layer to filehandles

Modern versions of Perl provide an even more powerful expansion of the idea of tied filehandles, called "PerlIO layers", where each layer between the program and the actual filehandle can call particular functions to manipulate the passing data. The non-hackish uses of this include doing encoding conversion and changing the newline format, all so that you can access them like so:

open $fh, '>:somelayer:someotherlayer:yetmore', 'file.dat'

as in:

open( $out, '>:utf8', 'resume.utf' ) or die "Cannot read resume: $!\\n"; print {$out} "\\x{2605} My R\\xE9sum\\xE9 \\x{2605}\\n"; close( $out );  # ^^-- a star character

The documentation for PerlIO::via, PerlIO, and Encoding describe the complex interface for writing layers. For super-simple layers, you can use a base class to reduce the interface to a single method, change. Here it is with two layer classes, Scream and Cookiemonster:

package Function_IO_Layer; # A dumb base class for simple PerlIO::via::* layers. # See PerlIO::via::dynamic for a smarter version of this. sub PUSHED { bless { }, $_[0] } # our dumb ctor # when reading sub FILL {     my($this, $fh) = @_;     defined(my $line = readline($fh)) or return undef;     return $this->change($line); } sub WRITE {     my($this,$buf,$fh) = @_;     print {$fh} $this->change($buf)  or return -1;     return length($buf); } sub change { my($this,$str) = @_;  $str; } #override! # Puts everything in allcaps. package PerlIO::via::Scream; use base 'Function_IO_Layer'; sub change {     my($this, $str) = @_;     return uc($str); } # Changes "I" to "me". package PerlIO::via::Cookiemonster; use base 'Function_IO_Layer'; sub change {     my($this, $str) = @_;     $str =~ s<\\bI\\b><me>g;     return $str; }

Use these layers as simply as:

open my $fh, '>:via(Scream):via(Cookiemonster)',    'author_bio.txt' or die $!; print {$fh} "I eat cookies without cease or restraint.\\n",    "I like cookies.\\n"; close($fh);

That will make author_bio.txt consist of:

ME EAT COOKIES WITHOUT CEASE OR RESTRAINT. ME LIKE COOKIES.

You can use PerlIO layers to operate on files you're reading (just change the '>' to '<') or appending to ('>>'), or even to alter data coming to or from processes ('-|' or '|-'). For example, this:

open my $ls, '-|:via(Scream)', 'ls -C /usr' or die $!; print <$ls>;

shows:

BIN  GAMES    KERBEROS  LIBEXEC  SBIN   SRC  X11R6 ETC  INCLUDE  LIB       LOCAL    SHARE  TMP

where a simple ls -C ~/.mozilla would show just:

bin  games    kerberos  libexec  sbin   src  X11R6 etc  include  lib       local    share  tmp

Going from encoding-conversion (open $fh, '>:utf8'...) to uppercasing (open $fh, '>:via(Scream)'...) may seem a leap from the sublime to the ridiculousbut consider that in between the two, intrepid CPAN authors have already written such classes as PerlIO::via::LineNumber, which transparently adds line numbers to the start of lines, or PerlIO::via::StripHTML, which strips HTML tagsall very hackish, and yet very useful.



Perl Hacks
Perl Hacks: Tips & Tools for Programming, Debugging, and Surviving
ISBN: 0596526741
EAN: 2147483647
Year: 2004
Pages: 141

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net