Intelligently shortcutting function maker

Aristotle on 2006-07-09T10:16:41

I often want to write short utility functions that do things like running a simple but frequently needed substitution on strings or the like. For these functions, I often want shortcutting behaviour: operate on $_ if not given a parameter, and operate destructively in-place if not asked to return values.

Writing functions that work like this is a real pain in the neck: there are multiple cases to be considered, you have to be careful to preserve the aliasing nature of @_ and $_, etc. It‘s way too much work to keep doing it over and over.

So here’s a function maker that lets you build such functions, carefully written to rely entirely on aliasing, rather than jumping through any reference-taking hoops. It expects to be given a reference to a function which always operates destructively on its parameters (ie modifies the elements of @_), and from this builds a function that will default to $_ as its input and will either return modified copies or modify its operands in-place.

Eg.:

BEGIN { *basename = shortcutted { s!.*/!! for @_ }; }

for( '/path/to/foo', '/some/path/to/bar' ) {
	print "Munging " . basename . "\n";
	open my $fh, '<', $_ or die $!;
	# note how $_ still contains the full pathname
	# ...
}

The code:

sub shortcutted(&) {
	my $sub = shift;
	sub {
		my @byval;
		my $nondestructive = defined wantarray;
		$sub->( $nondestructive
			? ( @byval = @_ ? @_ : $_ )
			: (          @_ ? @_ : $_ )
		);
		return $nondestructive ? @byval[ 0 .. $#byval ] : ();
	};
}

Tests:

use Test::More;

sub original() { 'original' }
sub modified() { 'modified' }

my $test = shortcutted { $_ = modified for @_ };

plan tests => my $num_tests;

{
	local $_ = original;
	$test->();
	is( $_, modified, 'in-place on $_' );
	BEGIN { $num_tests += 1 }
}

{
	local $_ = original;
	my $res = $test->();
	is( $_,   original, 'nondestructive from $_' );
	is( $res, modified, '...returned correctly' );
	BEGIN { $num_tests += 2 }
}

{
	my $num = 10;
	my @original = ( original ) x $num;
	my @modified = ( modified ) x $num;
	$test->( my @data = @original );
	is_deeply( \@data, \@modified, 'in-place on params' );
	BEGIN { $num_tests += 1 }
}

{
	my $num = 10;
	my @original = ( original ) x $num;
	my @modified = ( modified ) x $num;
	my @res = $test->( my @data = @original );
	is_deeply( \@data, \@original, 'non-destructive from params' );
	is_deeply( \@res,  \@modified, '...returned correctly' );
	BEGIN { $num_tests += 2 }
}


Lovely!

Dom2 on 2006-07-09T12:22:31

That's really useful. But having to use coderefs is a bit ugly. So I've added support for a :Shortcut attribute. The code is in my svn repository: Attribute-Shortcut/.
  sub basename :Shortcut { s!.*/!! for @_ }

  for ( '/path/to/foo', '/some/path/to/bar' ) {
      print "Munging " . basename . "\n";
      open my $fh, '<', $_ or die $!;
      # note how $_ still contains the full pathname
      # ...
  }

-Dom

Re:Lovely!

chromatic on 2006-07-09T20:40:17

I wish I'd thought of that. Very clever!

Re:Lovely!

Aristotle on 2006-07-09T23:12:12

Nice. I guess it’s worthy of being put on CPAN, after all. I’ll steal the attribute idea as that makes it much easier to use with named subs, but I still want the subref interface available for building anonymous ones.

Now I just need to think of a good name – shortcutted is fine for an off-the-cuff function, but it doesn’t meet my standards for something on CPAN…

Re:Lovely!

Dom2 on 2006-07-10T13:39:29

Feel free to steal the code as you see fit.

As to still wanting to do Subrefs, I think that you should be able to use Attributes with them as well. But I have no idea how, I'll need to play with that.

-Dom