Frustrated by autobox

stephenca on 2008-07-11T15:55:17

(Frustrated in a good way).

I am becoming increasingly fond of autobox, and particularly Scott Walters' extension for the core Perl built-in functions.

It brings to Perl syntax the sort of everything-is-an-object convenience and expressiveness that Rubyists and Smalltalkers get out of the box, e.g. you can do:

print [10, 20, 30, 40, 50]->pop(), "\n";
# prints 50

rather than the orthodox:

my(@list) = (10, 20, 30, 40, 50);
my($val) = pop(@list);
print $val, "\n";

You can also represent complex expressions in a more natural order (which also gives you a certain amount of self-documentation). As the autobox::Core documentation explains:

# Perl 5 - a somewhat complex expression
print join("\n", map { CGI::param($_) } @cgi_vars), "\n";

becomes

@cgi_vars->map(sub { CGI::param($_[0]) }) # turn CGI arg names 
                                          # into values
           ->join("\n")                   # join with newlines
           ->concat("\n")                 # give it a trailing 
                                          # newline
           ->print;                       # print them all out

It's obviously longer than the orthodox idiom, but there is greater clarity about the order in which the operations take place.

Alas, not everything in the garden is rosy. The autobox implementation of push() tickles an obscure Perl bug, with the consequence that you cannot pass an array to push(), e.g.:

my(@s) = ();
my(@aa) = (1,2);
@s->push( @aa );
# Bizarre copy of ARRAY in push at /usr/lib/perl5/site_perl/5.8.8/autobox/Core.pm line 710.
#
# It works with scalars:
for my $i ( 1,2,3 ) {
    @s->push( $i );
}
# @s is (1,2,3) 

You can workaround this with map():

my(@aa) = (1,2);
@s->push( map { $_ } @aa );
# @s is now ( 1, 2 )

but this is an ugly mixture of old and new syntax. Can we use autoboxed map()?

Well, by default, map() in autobox returns an arrayref; we can't use the @{[ .. ]} ref-deref trick (because this triggers the bug above), so we are snookered. However, we can use the flatten() method exported by autobox::Core (which does the equivalent of @{ $array_ref }).

(Note that the elements() method is identical to flatten. This isn't mentioned in the docs anywhere, though elements() is used in some examples).

my(@aa) = (1,2);
@s->push( @aa->flatten );
# @s is now ( 1, 2 )

Is this better than push( @s, @aa )? Not really.

An alternative workaround is to use autoboxed foreach() method and turn the expression inside-out:

my(@aa) = (1 ..3 );
@aa->foreach( sub { @s->push($_[0]) } );
# @s is now (1,2,3);

but the need for the anonymous subroutine and the use of the $_[0] notation means that this is no easier on the eye.

As an aside, the autoboxed map() supports two invocation styles:

Called on a subroutine reference, it takes the list to be mapped as an argument:

my($mapped_arrayref) = sub { return somefunc($_[0]) }
->map( @somelist );

and called on an array, it takes a subroutine reference as an argument:

my($mapped_arrayref) = @list
->map( sub { return somefunc($_[0]) } );