2012-05-11

Moose Types

After a while, you'll find that Perl's builtin data types, though useful, are limited. You will seek to expand them with a vast array of new data types and CPAN modules that add some kind of additional value to your program's logic: instead of open()'ing a file handle directly, you can abstract it objectively with IO::File.

Base Perl:

open(FILE_H, '< text.txt') or die($!);
print <FILE_H>;
close(FILE_H);

Object-oriented:

use IO::File;
defined(my $fh = IO::File->new('text.txt')) or die($!);
print <$fh>;
$fh->close;

There's some extensibility to the OO technique. For instance, you can still treat the IO::File object like a filehandle with <$fh> instead of explicitly using any of its available methods, like $fh->getline. In turn, IO::File gets a lot of its form and function by inheriting from IO::Handle, which gives a nice, mostly-consistent syntax between IO::File, IO::Dir, IO::Socket, and whatever else falls under the perview of input/output actions.

Then there's Moose. Moose is great and wonderful and is a holy, blameless creature. Problem is, Moose can be a little rough around the edges. This isn't entirely unexpected considering that it's a project whose purpose is to retrofit pieces of Perl6 on top of Perl5 because, damn it, Larry, we just can't wait that long. To be honest, object-orientation wasn't something with which Perl was designed or even, perhaps, intended. One can only imagine how hectic it would be to teach Perl5 Perl6-style OO.

It turns out that some intrepid folks have been extending Moose, which is something Moose is made to do. There are nearly as many MooseX extensions as there are stars in the sky and they all have really Linnaean taxonomic nomenclature: "MooseX::Types::This::That::Other" would not look out of place in a CPAN query for "MooseX". I found the whole thing a little daunting at first. In many ways I still do because these extensions are meant to take Moose — the extensible Perl6 OO of tomorrow, today — and extend it in obvious and not-so-obvious ways that are, themselves, Moosily extensible. The possibilities are endless, as is the potential for confusion and error.

Sometimes, it seems you can be too clever for your own good.

So how hard is it to read a file the Moosey way? There's more than one way to do it. We could wrap up any of our old techniques into an object and they will still work so long as we remember how to handle moving data around inside the class and pushing things through its celluar wall. Another option is to rely on MooseX type extensions and help make someone else's library do the heavy lifting. Doing it this way makes the code considerably longer since you have to account for the fact that before you open and read a file, you have to build the entire object around it. It's a little like Carl Sagan's apple pie recipe. ("If you wish to make an apple pie from scratch, you must first invent the Universe.") We don't want to make anything from scratch, though. Instead we want to leverage the MooseX types to wrap up a Path::Class object for us that is built directly into our object in a Moosey way.

package MyReadfile;
use Moose;
use MooseX::Types::Path::Class qw/File/;
has 'file' => (
  is       => 'ro',
  isa      => File,
  required => 1,
  coerce   => 1,
);

sub doit {
  my ($self) = @_;
  print $self->file->slurp;
}
1;
MyReadfile->new( file => 'text.txt' )->doit;

Is that sixy or what? In truth, Perl6 supports the easy way of doing this, too. It seems like a lot of extra typing relative to a simple open(), but compared to the effort we would need to commit to building a similar class from scratch in the base Perl syntax — making a constructor, blessing our referents, and writing our own read accessor all before worrying about actually getting and reading a file — it's relatively concise considering what we now have under the hood.

(If you are a masochist, you can still roll your own IO::File subtype and use it like so:)

package MyReadfile;
use Moose;
use IO::File;
use Moose::Util::TypeConstraints;

subtype 'MyFile' => as class_type('IO::File');
coerce 'MyFile'
  => from 'Str'
    => via { IO::File->new($_) };

has 'file' => (
  is       => 'ro',
  isa      => 'MyFile',
  required => 1,
  coerce   => 1,
);

sub doit {
  my ($self) = @_;
  print $self->file->getlines;
}
1;
MyReadfile->new( file => 'text.txt' )->doit;

(Coercions are global, meaning every Moose class in a given Perl instance shares them whether you want them to or not, so if you're not using the MooseX types you will have to keep your namespaces clean yourself. Hence why you want to define your own subtype "MyFile" and use that instead of IO::File directly.)

1 comment:

Sam said...

MyReadfile->new( file => 'text.txt' )->doit;

What if 'text.txt' doesn't exists?