Abusing "Memoize"

ferreira on 2008-03-05T20:13:03

A few days ago, I was writing a code (namely the Path-Classy dist) and stared at the code that produced the file size in raw bytes or "humanized" (28300 or 28K).

sub _format_size {
  my ($sz, $opt) = @_;

  my $format = $opt->{format};
  if ( $format eq 'h' ) {
    require Numbers::Bytes::Human;
    return Number::Bytes::Human->new->format( $sz );
  }
  else { # raw bytes
    return $sz;
  }
}

Of course, loading Number::Bytes::Human and creating a instance every time $f->size({ format => 'h' }) was invoked seemed overkill. But saving the N::B::H into a class/instance variable seemed overkill too: it has nothing to do with Path::Classy (which are Path::Class) objects but for that instant relationship to format a file property, size.

Hey, that's a chance to use memoization, splitting the formatter creation into a function and then memoizing it (so that we don't need to create a [reusable] object with the same capabilities over and over), we come to this code.

use Memoize;

sub _size_formatter {
  require Number::Bytes::Human;
  return Number::Bytes::Human->new;
}
memoize('_size_formatter');

sub _format_size {
  my ($sz, $opt) = @_;

  my $format = $opt->{format};
  if ( $format eq 'h' ) {
    return _size_formatter->format( $sz );
  ...

That looked elegant to me. To make it even more tight (and to require yet another CPAN module ;-) ), using Attribute::Memoize seemed right. It avoids the need to repeat the function name in the memoize call and it anticipated the wrapping up of the sub to BEGIN time (a free bonus of Attribute::Handlers in the backstage).

use Attribute::Memoize;

sub _size_formatter :Memoize {
  require Number::Bytes::Human;
  return Number::Bytes::Human->new;
}

That's it! Efficient code, localized behavior, no need for extra variables. Will people understand that for maintenance? I hope so.