Hack 92. Return Smarter Values


Choose the correct scalar for any context.

There's always one troublemaker in any bunch. When it comes to return contexts, that troublemaker is scalar.

List and void contexts are easy. In list context you just return everything. In void context, return nothing. Scalar contexts allow you to return only one thing, but there are just too many alternatives: a string, a count, a boolean value, a reference, a typeglob, or an object.

The real problem, though, isn't actually that there are too many types of possible return value in scalar context; the real problem is that Perl simply doesn't provide you with enough...well...context with which to decide. The only basis you have for knowing whether to return a string, number, boolean, and so on, is receiving a single uninformative defined-but-false value from wantarray.

Even then, using wantarray leads to a lot of unnecessary and self-undocumenting infrastructure:

if (wantarray)                # wantarray true      --> list context {     return @some_list; } elsif (defined wantarray)     # wantarray defined   --> scalar context {     return $some_scalar; } else                          # wantarray undefined --> void context {     do_something( );     return; }

It would be much easier if you could just specify a single return statement that knew what to return in different contexts, perhaps:

return     LIST   { @some_list     }     SCALAR { $some_scalar   }     VOID   { do_something( ) };

That's exactly what the Contextual::Return CPAN module does. It makes the previous example work like you'd expect.

Fine Distinctions

The module also allows you to be more specific about what to return in different kinds of scalar context.

For example, you might want a stopwatch( ) subroutine that returns the elapsed time in seconds in numeric contexts, but an HH:MM:SS representation in string contexts. You might also want it to return a true or false value depending on whether the stopwatch is currently running. You can do all of that with:

use Time::HiRes 'time'; use Contextual::Return; my $elapsed       = 0; my $started_at    = 0; my $is_running    = 0; # Convert elapsed seconds to HH::MM::SS string... sub _HMS {     my ($elapsed) = @_;     my $hours     = int($elapsed / 3600);     my $mins      = int($elapsed / 60 % 60);     my $secs      = int($elapsed) % 60;     return sprintf "%02d:%02d:%02d", $hours, $mins, $secs; } sub stopwatch {     my ($run)     = @_;     # Update elapsed time...     my $now       =  time( );     $elapsed     +=  $now - $started_at if $is_running;     $started_at   =  $now;     # Defined arg turns stopwatch on/off, undef arg resets it...     $is_running   =  $run if @_;     $elapsed      =  0 if @_ && !defined $run;     # Handle different scalar contexts...     return          NUM { $elapsed         }          STR { _HMS( $elapsed ) }         BOOL { $is_running      } }

With that arrangement, you can write code like:

print "The clock's already ticking\\n"     if stopwatch( );                              # treat as a boolean stopwatch(1);                                    # start do_stuff( ); stopwatch(0);                                    # stop print "Did stuff in ", stopwatch( ), "\\n";        # report as string stopwatch(undef);                                # reset stopwatch(1);                                    # start do_more_stuff( ); print "Did more stuff in ", stopwatch(0), "\\n";  # stop and report print "Sorry for the delay\\n"     if stopwatch( ) > 5;                          # treat as number

Name that Return Value

The stopwatch example works well, but it still doesn't explore the full range of possibilities for a scalar return value. For example, the single piece of numeric information you want back might not be the elapsed time, but rather when the stopwatch started. You might also want to return a boolean indicating whether the stopwatch is currently running without always having to cast your call into boolean context:

$stopwatch_running = !!stopwatch( );      # !! --> boolean context

It would be handy if, in addition to all the other return options, stopwatch( ) would also return a hash reference, so you could write:

$stopwatch_running = stopwatch->{running}; print "Stopwatch started at ", stopwatch->{started}, "\\n";

Returning a hash reference allows you to send back all the information you have available, from which the caller can then pick out (by name) the interesting bits. Using names to select what you want back also helps the code document what it's doing.

Contextual::Return makes it easy to add this kind of behavior to stopwatch( ). Just add a specific return value for the HASHREF context:

# Handle different scalar contexts... return         NUM { $elapsed         }         STR { _HMS( $elapsed ) }        BOOL { $is_running      }     HASHREF { { elapsed                 => $elapsed,                                started   => $now - $elapsed,                                running   => $is_running,                              }                            }             

Out, Out, Damn Commas!

Contextual::Return can handle other types of reference returns as well. One of the most useful is SCALARREF {...}. This block specifies what to return when the calling code uses the return value as a reference to a scalar. That is, what to return if you write:

${ stopwatch( ) }    # Call stopwatch( ) and treat result as scalar ref

The reason this particular construct is so interesting is that you can interpolate it directly into a double quoted string. For example, add a SCALARREF return block to stopwatch( ):

# Handle different scalar contexts... return         NUM { $elapsed         }         STR { _HMS($elapsed)   }   SCALARREF  { \\ _HMS($elapsed)   }        BOOL { $is_running      }     HASHREF { {   elapsed => $elapsed,                   started => $now - $elapsed,                   running => $is_running,               }             }

Then, whenever it's called in a scalar-ref context, the subroutine returns a reference to the HH:MM:SS elapsed string, which the scalar ref context then automatically dereferences. Instead of having to write:

print "Did stuff in ", stopwatch( ), "\\n";

you can interpolate the call right into the string itself:

print "Did stuff in ${stopwatch( )}\\n";

This turns out to be so amazingly useful that it's Contextual::Return's default behaviour. That is, any subroutine that specifies one or more of STR {...}, NUM {...}, or BOOL {...} automatically gets a SCALARREF {...} as well: one that returns a reference to the appropriate string, number, or boolean.



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