POOL
by Simon Cozens
|
Pages: 1, 2
Another Example
Now that I had this neat tool for generating modules, I set it to work
on the next module I wrote; this was a variant of Tree::Trie, a
class to represent the trie data structure. Tries are a handy way of
representing prefix and suffix relationships between words. They're
conceptually simple; each letter in a word is inserted into a tree
as the child of the previous letter. If we wanted a trie to count the
prefices in the phrase THERE IS A TAVERN IN THE TOWN, then we would first
insert the chain T-H-E-R-E-#, then I-S-#, then A-#, and so on,
where # represents "end-of-word". We'd end up with a trie looking
like this:
|
| Figure 1: An Example of a Trie |
Tree::Trie is good at this sort of thing, but it didn't do a few
extra things I needed, so I wrote an internal-use module called
Kasei::Trie; this was the POOL file I used to generate it:
Kasei::Trie - Implement the trie data structure
DESCRIPTION
"Trie"s are compact tree-like representations of multiple words, where
each successive letter is introduced as the child of the previous.
EOD
@children {}
insert
Kasei::Trie::Node
@children
->@data
The main class, Kasei::Trie, has a constructor with one instance variable
that is initialized to be an empty hash reference, and one method, insert.
There's also a secondary class representing each node in the trie, which has
its own children, and has a data variable with its own accessor.
After generating this with POOL, all I needed to do was to fill in the
code for the insert method, and modify some tests. A manifest,
Makefile.PL, test suite with nine tests, and 161 lines of code and
documentation were automatically created for me. I suspect that POOL
saved me one to two hours.
The High Dive
Let's now take a look at the templates that make this all happen. The
main template is called module, and it looks like this:
package [% module.package %];
[% INCLUDE module_header %]
=head1 SYNOPSIS
[% INCLUDE synopsis %]
=head1 DESCRIPTION
[% module.description %]
=head1 METHODS
[%
FOREACH method = module.methods;
INCLUDE do_method;
END
%]
[% INCLUDE module_footer %]
1;
As you can probably guess, in the Template Toolkit language, interesting
things happen between [% and %]. Everything else is just output
wholesale, but all kinds of things can happen inside the brackets. The
first thing that happens is that we look at the module's package name.
All the data we've collated from the parsing phase is stuffed into a hash
reference, which we have passed into the template as module. The
dot operator in Template Toolkit is a general do-the-right-thing operator
that can be a method call, a hash reference look-up or an array reference
look-up. In this case, it's a hash reference look-up, and
we perform the equivalent of $module->{package} to extract the name.
Template Toolkit's [% INCLUDE template %] directive looks for the file
template in its template path, processes it passing in all the relevant
variables, and includes its output. So after the initial package ...; line,
we include another template that contains everything that goes at the top of
the module. As we'll see later, part of the beauty of templating things this
way is that you can override templates by placing your own idea of what should
go at the top of a module into your private version of module_header earlier
in the template path, in a sense "inheriting" from the base set of templates.
Similarly, we include a file that will output the synopsis, and output the
description that we collected between the DESCRIPTION and EOD
lines of our POOL definition file.
Next, we want to document the various methods and output the code for
them. POOL will have placed all the metadata for the methods we've
defined, plus a constructor, in the appropriate order in the methods
hash entry of module. As this is an array reference, we want to use a
foreach-style loop to look at each method in turn. Not surprisingly,
Template Toolkit's foreach-style loop is called FOREACH.
So this code:
[%
FOREACH method = module.methods;
INCLUDE do_method;
END
%]
will set a variable called method to each method in the array, and
then call the do_method template. This simply dispatches to
appropriate templates for each type of method. For instance, there's the
set of templates for the "delegate" style; delegate_code looks like
this:
sub [% method.name %] {
my $self = shift;
return $self->[% method.via %]->[% method.how %](@_);
}
Whereas the documentation template contains some generic commentary:
=head2 [% method.name %]
[% INCLUDE delegate_synopsis -%]
Delegates to the [%method.how%] method of this object's [%method.via%].
=cut
The synopsis that appears in the documentation here and in the synopsis at the top of the file simply explains how the delegation is done:
$self->[% method.name %](...); # $self->[% method.via %]->[%method.how%]
Of course, there are some templates that are a little more complex, particularly those that generate the tests, but the main thing is that you can override any or none of those. If you don't like the standard same-terms-as-Perl-itself licensing block that appears at the end of the module, then create a file called ~/.pool/license containing:
=head1 LICENSE
This module is licensed under the Crowley Public License: do what thou
wilt shall be the whole of the license.
POOL will pick up this template and use it instead of the standard one.
There's No P in Our POOL
When I started planning this article in the bath this morning, I realized that POOL is actually fantastically badly named; there's nothing actually Perl-specific about the language itself, and it's a handy definition language for any object-oriented system. Hence, I hereby retroactively name the project "the POOL Object Oriented Language", which also satisfies the recursive acronym freaks. But can we, using the same parser and templating system, turn POOL files into other languages? Of course we can; this is all part of the flexibility of the Template Toolkit system. What's more, we don't even have to override all of the templates in order to do so, just some of them. For instance, here's a Ruby equivalent of accessor_code:
[% IF method.ro == "ro"; %]
attr_reader :[% method.name %]
[% ELSE; %]
attr_accessor :[% method.name %]
[% END; %]
do_method and module_footer, however, never need to change, since all they do is include other methods. With a complete set of toolkits, the same POOL description can be used to output a Perl, Ruby, Python, Java and C++ implementation of a given class.
Going Deeper
When Frans Hals' famous painting "The Laughing Cavalier" was being examined in a museum's labs, someone had the bright idea of putting it through an X-ray machine. When they did this, they were amazed to find underneath the famous painting a completely different work -- a painting of a young girl. They then adjusted the settings on the X-ray machine and tried again, and underneath the young girl, they found another painting. Since then, it's been common practice to X-ray pictures, and art historians have found many layers of paint underneath some of the most-famous pictures.
What's this got to do with POOL? Well, very little, but I wanted to throw that in. Since I realised that POOL's templates can be inherited so easily, I've had the idea of POOL "flavors"; coherent sets of templates that can be layered like oil paintings to impart certain properties to the output.
For instance, at the moment, POOL outputs unit tests in separate files in
the t/ directory, one for each class. Some people, however, prefer
to have their tests in the module right alongside the documentation and
implementation, using the method described in Test::Inline. Well,
there's no reason why POOL shouldn't be able to support this. All you'd
need to do is create a new directory, let's say testinline/, and put
a modified version of do_method in there which says something like:
[% INCLUDE method_pod %]
=head2 begin testing
[% INCLUDE method_test %]
=cut
[% INCLUDE method_code %]
Next, arrange for testinline/ to appear in the Template Toolkit template path, and magically your tests will appear in the right place.
It's not inconcievable that multiple "flavours" could combine in order
to theme a module; for instance, you might want a module which uses
Test::Class for its tests, and Module::Build for its build file,
with a BSD license flavor and Class::Accessor for its accessors
instead of having them explicitly coded. Conceptually, you'd then say:
pool --flavours=testclass,modulebuild,bsdlicense,classaccessor mymodule.pool
and the module would come out just as you want. This hasn't happened yet for two reasons: First, although it's only a two- or three-line change to the pool parser to support pushing these directories onto the template path, I haven't needed it yet so I haven't done it, and second, because I haven't written any flavors yet. But it's easy enough to do.
Other future directions for POOL include a syntax for class methods and
class variables, support for other languages as mentioned about, (which
basically means ripping out the hard-coding of MANIFEST,
Makefile.PL and so on and replacing that with a more flexible method)
and other minor modifications. For instance, I'd like some syntax to
specify dependencies; other Perl modules which will then be use'd in
the main modules and which would be named at the appropriate place in the
Makefile.PL. And, of course, there's building up a library of
flavors, including "total conversion" flavors like Ruby and Python.
The one thing that's becoming really, really important is the need for nondestructive editing -- the ability to fill in some additional code for a method, then regenerate the class from a slight change to the POOL file without losing the new method's code. I'm going to need to add that soon to allow for iterative redesigning of modules.
But the main thing about POOL is what it does now -- it saves me time, and it takes away the drudgery of writing OO classes in Perl.
And I will finish Devel::DProfPP soon. I promise.

