Classy code generation
Today I was asked "how do I create a class on the fly?" and realised that I know several ways to do that and which one I'd use varies depending on the situation. So I thought I'd share the answers.
The old fashioned way
It's entirely possible to generate a class using no modules at all and basic perl typeglob craziness. So, to generate a class called $target we do:
{
no strict 'refs';
@{"${target}::ISA"} = @superclasses;
}
(the block around it is so that the removal of part of strict doesn't escape)
and then we can add methods using
use Sub::Name;
my $full_name = "${target}::${method}";
{
no strict 'refs';
*$full_name = subname $full_name, $sub_ref;
}
($sub_ref needs to be defined outside the block so that -it- isn't compiled without strict refs, and the subname() call means that perl sees the final installed code as having the correct name, which makes for better stack traces)
If this seems a little light on explanation ... that's because so far as I'm concerned it's very much -not- the right way to do it for most purposes, since there's plenty of libraries that wrap this up for you. It is, however, illustrative in that pretty much every other technique I'm going to describe ends up doing this or something very like it under the hood.
Anyway: onwards.
The full-on MOP way
Of course, if you've already played around with such things, then using Class::MOP or realistically Moose's meta-protocol to do the generation work would come to mind as a likely option - and indeed it is an option, and not a bad one at all:
my $meta = Moose::Meta::Class->create(
$class_name,
version => $version,
superclasses => \@superclasses,
roles => \@roles,
methods => { method_name => $sub_ref, ... }
);
Note here that everything except for the class name is optional and (except for the version) can be supplied later instead, so:
my $meta = Moose::Meta::Class->create($class_name);
$meta->superclasses(@superclasses);
Moose::Util::apply_all_roles($meta, @roles);
$meta->add_method(method_name => $sub_ref);
Methods added this way will automatically get Sub::Name used on them, and we can nicely escape all of the strict rubbish since there's code down in the guts handling that for us.
Plus, at this point we have access to the attribute system, so
$meta->add_attribute(name => (is => 'ro', default => 'Gemma'));
will work and do pretty much exactly what you'd expect. Just remember that once you're done, as with all Moose classes, you should call
$meta->make_immutable;
to tell Moose you're done so it can turn on the relevant optimisations.
So, when would I use this approach? In a project that already depended on Moose, I'd give it serious consideration as the first approach to use, especially when the logic of deciding -what- to generate is complicated and the generation itself only a small part of what's going on.
The "you did what?!" way
Another approach that I've used before now goes something like this:
use Template::Tiny;
use B qw(perlstring);
my $args = {
class => $class_name,
superclasses => [ map perlstring($_), @superclasses ],
attributes => [
{ name => 'name', default => perlstring('Gemma') },
...
],
methods => [ { name => $name, body => $code }, ... ]
};
Template::Tiny->new->process(<<'END', $args, \my $output);
package [% class %];
use Moose;
extends [% superclasses.join(', ') %];
[% FOREACH a IN attributes %]
has [% a.name %] => (is => 'ro', default => [% a.default %]);
[% END %]
[% FOREACH m IN methods %]
sub [% m %] {
[% body | indent(2) %]
}
[% END %]
__PACKAGE__->meta->make_immutable;
1;
END
eval $output or die "Failed to eval code: $@";
which has a tendency to make people go "WHAT?!".
So, first, the only odd thing here is perlstring - this is a function that takes a string and formats it as a string for perl source code - so
perlstring("Foo"); # returns '"Foo"'
perlstring("Foo
Bar"); # returns '"Foo\nBar"'
which means we can safely push the output straight into code.
More importantly, why would I do it this way? Two reasons: When the class itself is potentially relatively complex and we expect to be debugging the behaviour of the class, being able to warn() straightforward perl source code can be a huge advantage in terms of figuring out what the mistake was.
Also, if you ever think you might want to generate the results of this out to disk (for example to provide a DBIx::Class::Schema::Loader style interface or to produce a scaffold for the user to edit) then you've already got the source code and you already know it works.
So this isn't a technique to use every day, but it can be extremely useful.
The Package::Variant way
Of course, if you're not already using Moose and you don't want to do things the old fashioned way, you run into a bit of a problem. When people started asking about parameterised roles for Moo I decided to stop and think about it and figure out what the general problem was.
It occurred to me that most of the things I wanted to do were basically versions of:
package GeneratingThis;
use Some::Thing qw(some_sub);
some_sub(@$_) for @{$inputs_for{some_sub}};
plus some straightforward method installation.
This resulted in a module called Package::Variant which lets me write
package My::Class::SomethingOrOther;
use Package::Variant
importing => 'Moo',
subs => [ qw(extends with has before after around) ];
sub make_variant {
my ($class, $target, %args) = @_;
extends @{$args{superclasses}};
foreach my $attr (@{$args{attributes}}) {
has $attr->{name} => (
is => 'ro',
default => sub { $attr->{default} }
);
}
install method_name => sub { ... };
}
1;
where install() will do the *{} part for you, and uses Sub::Name if it's available (this means that Package::Variant remains pure perl by default so can be used with App::FatPacker, Object::Remote etc.).
and then use it like -
use My::Class::SomethingOrOther;
my $class_name = SomethingOrOther(
superclasses => [ 'Base::Class' ],
attributes => [ { name => 'name', default => 'Gemma' } ],
...
);
my $object = $class_name->new(...);
This approach works pretty well on the whole; you can generate pretty much anything using it - including Moo::Roles, of course - and to be entirely honest I like that it's not entirely pretty; that acts as a good reminder that I'm fundamentally doing something kind of crazy (generating classes in the first place).
In summation
If you're already using Moose and only generating classes/roles, you should probably be using Moose::Meta::Class/Moose::Meta::Role.
If you're generating lots of similar classes and either expect to spend lots of time debugging the generated rather than generator logic, or want to be able to scaffold files on disk, use the template generation approach.
If you're generating more than just M* classes/roles, and/or don't want this code to take a hard dependency on Moose, use Package::Variant.
Hope you enjoyed this little exploration. Until next time:
-- mst, out.