A tale of two languages 23
While I was at OSCON I found myself chatting with various non-perl and ex-perl folks and many of them had the same impression of Perl as a great language for hacking out short scripts to get stuff done, right now and possibly under severe time pressure. For these people, those scripts would very rapidly become unmaintainable. And it’s easy to see why they came to that conclusion. Perl was originally designed and implemented by a systems administrator as a tool to make his every day tasks easier so there are a bunch of shortcuts and defaults that do exactly that – make Larry’s life in 1987 easier. Which is great when you’re whipping up a script to meet an immediate demand, but horrendously cryptic when you look at it later.
This is a perfectly valid way of looking at a language that I’ll call ‘desperate perl’. Desperate Perl scripts tend to be written by folks who don’t necessarily know all of Perl and who certainly don’t know all of its ecosystem. The scripts are often written to a tight deadline and to handle a specific task. They certainly weren’t written with even half an eye to maintenance. And they live on. Boy, do they live on. There are CGI scripts written in 1995 that are still in every day use, never mind that they’re buggy, insecure and well nigh impossible to maintain, still these zombies shamble on, colouring the opinion of everyone who ever has to deal with them.
If there’s one thing we’ve learned from this, it’s that code is forever. When we were young, we did stupid things, but we grew up and everyone but our nearest and dearest (who have the good grace never to mention it) forgets that, once upon a time, we set fire to the living room rug while playing with matches/ate cornflakes with HP sauce/had a brief flirtation with the young Conservatives/had a brief flirtation with a young Conservative. We are forgiven.
Not Perl.
Perl’s reputation as a language that’s fine for writing short scripts, but which rapidly becomes unmaintainable once you try and write larger systems is bestowed upon it by people who looked at formmail.pl once and ran screaming into the night (a not unreasonable response).
But Desperate Perl isn’t the only language in the binary. Large scale programming with Perl uses, or should use, a different subset of the language. A key indicator that you’re looking at large scale Perl is when the script or library starts with something along the lines of:
#!/usr/bin/perl use strict; use warnings;
or, if it’s a library:
package Whatever; use Moose;
There are other signifiers, but the important thing is that the code is written in strict compliant Perl. use strict crops up so often that the recent crop of modern (or enlightened if you prefer) perl modules, like Moose, turn it on by default. Indeed, if you’re using Perl 5, version 12 and you begin your code with use 5.12.0, Perl turns on strictures as well as enabling all the new features of the language.
We’ve had strict and warnings since forever and they’ve been strongly recommended for everything longer than about two lines since forever too. Those of us who program in Perl for a living get rather annoyed by the ‘Desperate Perl’ blinkers worn, however unwittingly, by so many outside our community. We write our code in a subtly different language. We write tests first, we have done for years. We write clean, maintainable, object oriented code. Nowadays we use Moose to help us with that; Perl’s out of the box OO features are, let us say, idiosyncratic – Moose makes our lives better.
Large Scale Perl is about more than just what’s in the distribution. Large Scale Perl has a wider culture than just the contents of the distribution. It’s about learning to use the CPAN and the CPAN toolchain. When I start to work on anything perl related that I expect to live for more than I couple of minutes, I start by doing:
$ mkdir ProjectName $ cd ProjectName $ mkdir t lib $ $EDITOR t/initial.t
Because I’m writing large scale perl, my modules go under lib, my tests go under t, any scripts I expect to install in my path go in script and they don’t have a .pl suffix. I lay my code out this way because, well, that’s just how it’s done. I run my tests from the project directory by doing prove -l ./t and, when I realise I have something that’s worth distributing (or deploying to the staging server), I add a Module::Install based Makefile.PL that lists my prerequisites, the scripts to install and various bits of metadata. By now, this is second nature – many clever people over the years have come to a collective decision about what a good Perl project looks like and they’ve written a fabulous toolchain for me as well.
If I distribute my work through the CPAN, there’s a large community of CPANTesters who will attempt to build my module and run its tests on a bewildering variety of platforms, and they’ll send me bug reports when those tests fail (bug reports which I can opt out of as well). Any documentation I write will be nicely formatted and accessible through search.cpan.org along with links to sites that’ll draw me a tree of all the modules mine depends on and, if it becomes adopted by the community, nice lists of all the modules that depend on mine as well. My module will get its own section in rt.cpan.org so there’s a standard place for people to report bugs.
Large Scale Perl comes with an entourage. Tools like Perl::Critic can check my code for bad style, Try::Tiny helps me get exception handling (which is annoyingly tricky when you attempt to get it right barehanded) right, Devel::NYTProf helps me work out why it’s running slower than I expected. The list goes on.
But, all the world sees is Desperate Perl. Search for “Perl Tutorial” on google and the first few hits are horrendously out of date – when I checked, the number one hit was all about perl 4. Apparently, O’Reilly’s biggest selling Perl title is the new edition of Learning Perl, which was published in 2008 and is a cracking tutorial. But the second biggest Perl title is Programming Perl which is ten years old this month. It covers Perl 5 version 6.0. I’m not even sure you can still build perl 5.6.2 – released more than 6 years ago – on modern systems. I certainly can’t imagine why you might want to. Programming Perl is a great book, but there are things it discusses that aren’t just deprecated, they’ve been recognized for the clusterfuck they were and excised from the language with extreme prejudice. As a community, we could really use a new edition. It would seem to make sense for someone to take the job on. A computer book that’s still selling 10 years after its first publication, 8 years after the language it documents, 5.6 was superceded by 5.8 looks like an ideal candidate for a new edition to me.
But then, a glance at the release dates of major Perl versions shows us why O’Reilly might hesitate to revise the book. 5.6 got released in March 2000, 5.8 in July 2002, 5.10 in… December 2007, 5.10.1 (which might have been better named as 5.12.0, given how much got changed/fixed) in August 2009, 5.12 in April 2010. That 5 year gap is the sort of thing that might give any publisher pause. Still, the new strategy of releasing regularly and often is starting to bed in. It’s getting easier to write perldelta.pod for each release and Jesse Vincent seems to be doing an excellent job of getting new release managers trained up and generally destressing the business of releasing a new perl. Who knows, by this time next year the perl release process may be no more difficult than releasing a module to CPAN. We do write good tools after all.
Reliable, regular releases with no big surprises are the sort of thing that gives programmers and organizations confidence that we’re here for the long haul. We perl programmers know that all sorts of good things were happening on the CPAN in the long gap between 5.8 and 5.10, but as good as it is, CPAN’s not as visible as the version number on the binary.
Hmm… I appear to have rambled some way from my starting point. Which probably means its time to stop. If I’m spared, I might manage to rustle up some kind of conclusion in a later post. Just remember, there are at least two perls: Desperate Perl, and Large Scale Perl. Both are excellent languages in their spaces, but you can’t (or shouldn’t) draw conclusions about one based on observations of the other.
Falling out of love with a language 24
So, Giles Bowkett asked me on facebook “Why Perl?”. This is the long answer.
I’m a Perl hacker. I have been for around 16 years now. Around 5 years ago, prompted by the Pragmatic Programmer and Adam Turoff, I started looking at Ruby, and Ruby on Rails and sort of fell into maintaining Typo.
Why? I was getting hacked off with Perl.
I was coming to the end of my tenure/tether as Perl 6 Summarizer: watching a language that I still want to use before I die taking forever to get done gets wearing after a while, especially when you’re spending 8 hours a week summarizing the activities of a development community that, in parts, was verging on the toxic (it’s way better now).
I was also getting annoyed by small niggles in the way Perl 5 works. This was in the days before Moose. Stevan Little was just starting work on what became Class::MOP (as a prototype of Perl 6’s metaobject protocol) and way before the days of Devel::Declare.
Ruby seemed to be, as Matz has described it, “Perl, done right”. All the things that were pissing me off about Perl were so much easier in Ruby. I’ve joked, in a Ha Ha, only serious kind of way, that I left Perl for Ruby because I got fed up of unrolling @_, but there’s more to it than that. “Objects everywhere” fits my head. The language is dynamic. Ruby code had less boilerplate in it. Oh, and Ruby on Rails was looking very cool, even if David Heinemeier Hansson didn’t seem to have a clue about RESTful/Resourceful principles (he has rather more of a clue now). The community looked vibrant. What’s not to like?
I was a very happy Ruby programmer. It’s a great language. If you haven’t taken it out for a spin round the block, you should give it a try.
A Perl 5 rennaissance
While I was away on my failed attempt to become a maths teacher and then my Ruby sojourn, Perl 5 was waking up. If Perl 6 were declared a failure tomorrow by the people who are actually working on it (as opposed to the people who aren’t working on it, but bitch about it anyway – see some of chromatic’s more intemperate posts about those people), then it will still have had value for the way it’s inspired members of the Perl 5 community to nick the good bits and make them happen in Perl 5.
Stevan finished Class::MOP and it got used as the basis for Moose, which is essentially a layer of sensible defaults which sits on top of the insanely configurable and slightly more agnostic Class::MOP, which in turn sits on Perl’s weird reduced instruction set object orientation. Moose even got fast enough that it’s usable in polite company (it was always faster than Ruby), so people did. It grew an ecosystem.
Another, more recent development, is Devel::Declare. Devel::Declare is, in accordance with the long history of Perl, completely batshit insane. What it does is lets you trip the perl parser up and, while it’s standing there looking at the pretty birds flying around its head, you can run ahead and rewrite the code that it’s about to parse. Well, I say run, but what I mean is hobble ahead wearing narrow blinkers and mittens. You can accomplish amazing things in such circumstances, but it’s not fun. Devel::Declare enabled my current favourite module on CPAN: MooseX::Declare, which I’ve talked about here:
An introduction to MooseX::Declare from Piers Cawley on Vimeo.
MooseX::Declare lets me kill boilerplate. Instead of writing:
package Something;
use Moose;
has an_attribute => (is => 'rw');
sub a_method {
my($self, $with, $parameters)
= @_;
...
}I can write:
use MooseX::Declare;
class Something {
has an_attribute => (is => 'rw');
method a_method($with, $parameters) {
...
}
}which makes me happy. This is being done by a module which is written in Perl. It relies on a couple of modules that are implemented in C, but the bulk of the work is done in Perl itself.
So, that’s addressed one of the annoyances that led me to Ruby. What about ‘objects everwhere’. I got used writing things like:
5.times { |each| ... }Surely I’m not going to able to write:
5->times(sub {...})in perl?
Well…
use Moose::Autobox;
sub Moose::Autobox::SCALAR::times {
my($count, $block) = @_;
1->to($count)->each_key($block);
}
5->times(sub {$_->say});Bingo! I’d like to be able to write 5->times { $_->say } but can’t quite manage it. Give PerlX::MethodCallWithBlock a little more time though…
So, while I was away, most of the issues I had with Perl, issues that had driven me into the arms of Ruby, had been addressed.
It’s not about the bike
The new Perl tech is great, but all it does is removes barriers. I wasn’t attached to the Perl language by some huge bungee cord which was crushing me against these barriers while I sojourned in Ruby land, all the while pining for Perl. I was loving Ruby. It’s a great language.
What I am attached to is the group of friends I’ve made in the Perl community over the years. Many of whom I’ve never actually met face to face.
I felt like such a curmudgeonly old fart in the Ruby community (more specifically, the Rails community). Everything’s shiny and new and awesome. And, I suppose when you’re coming to the language from something like PHP, it is shiny and new and awesome. But it gets wearing after a while. And the arrogance? My dear! The arrogance! Admittedly, some people have a lot to be arrogant about – I still cheer DHH’s “Fuck you!” slide, but others… not so much. The kind of “Look! Ruby is uniquely suited to writing DSLs!” bullshit1 that sends chromatic off in fits of apoplexy annoyed me too, especially when I was looking at the kind of examples they were presenting as exemplary and remembering Perl from 5 years before that did the same thing but with cleaner language-like interfaces.
Coming back to Perl may well be a straightforward retreat to competence. I may be rationalizing like mad. But right now, it feels like I left Ruby because the ruby community, in the West at least, isn’t a fun place for me to be. The sexism is just the icing on that particular cake.
1 Here’s my response to Rspec:
testclass exercises Something {
test that we can create an object {
isa_ok $test->subject->new, 'Something';
}
}Note the lack of perlish furniture in the test that we can create an object part. No need to quote the strings, no meaningless dos scattered about the place, no :symbols appearing at random. Okay, so I have to quote 'Something' in the implementation block, but the implementation block is unadulterated Perl. I don’t claim that only Perl can do this. I do claim that, right now at least, Ruby can’t.
Class decomposition and a handy delegation pattern 3
There’s something satisfying about reaching the point when you can’t decompose an object any further and all your methods are tiny and do one thing – it’s especially gratifying when you learn something new in the process. Sadly, it doesn’t happen as often as I’d like, there’s usually annoying bits and pieces where you have to placate the language in some fashion that breaks the flow of what you’re writing.
As I get a better handle on the way MooseX::Declare has changed Perl, I’m finding I have to do much less in the way of placation.
Here’s an example. For context, I’m writing a traffic shaping tool. The basic client interface needs to look something like:
$policy->current_weight # => a percentage between 0 and 100
Not much of an interface really. The requirements state that we should be able to specify weights with 15 minute granularity for every day of the week. Our problem becomes one of mapping from a time to a number between 0 and 7 (days) * 24 (hours) * 4 (quarters) - 1 and looking up the weight in an array.
Here’s my first cut:
use MooseX::Declare;
class WeightVector {
use DateTime;
has vector => (
isa => 'Array[Int]',
is => 'ro',
required => 1,
);
method current_weight {
my $now = DateTime->now;
my $offset = ($now->wday_0 * 7 * 24 + $now->hours) * 4 + int($now->minutes / 15);
return $self->vector->[$offset];
}
}Which is, I suppose, perfectly respectable. However, current_weight isn’t filling me with delight. First it finds the current time, then it converts the time into an offset, then it uses the offset to lookup the weight in the vector. Let’s introduce a method to find the weight at a specific time"1":#decomp-motivation, the relevant code becomes:
method current_weight {
$self->weight_at(DateTime->now);
}
method weight_at (DateTime $time) {
my $offset = ($now->wday_0 * 7 * 24 + $now->hours) * 4 + int($now->minutes / 15);
return $self->vector->[$offset];
}And again, we could rest here, but again, we’re doing two things. We’re converting from a time to an offset, then we’re looking up the value in the vector. Type conversions tend to happen again and again, so it’s good if we can specify them separately. We could write a time_to_offset helper method, but we’re in Mooseland now; there’s a better way. Let’s introduce a formal Moose type and define a coercion for it. Here’s the type definition stanza of the code. I’ve taken the opportunity to add types which do bounds checking for the vector as well, while I’m about it.2
use MooseX::Declare;
class WeightVector {
use DateTime;
use Moose::Autobox;
use MooseX::Types -declare => [qw(SlotOffset VectorOfWeights PercentageInt)];
use MooseX::Types::Moose qw(ArrayRef Int);
use constant TOTAL_SLOTS = 7 * 24 * 4;
BEGIN {
subtype PercentageInt,
as Int,
where { 0 <= $_ && $_ <= 100 },
message { "$_ does not is not an integeter between 0 and 100" };
subtype VectorOfWeights,
as ArrayRef[PercentageInt],
where { $_->length == TOTAL_SLOTS }
message { "Vector must have ".TOTAL_SLOTS." entries, not ".$_->length };
subtype SlotOffset,
as Int,
where { 0 <= $_ && $_ < TOTAL_SLOTS };
class_type 'DateTime';
coerce SlotOffset,
from 'DateTime',
via { ($_->wday_0 * 7 * 24 + $_->hours) * 4 + int($_->minutes / 15) };
# Let's allow clients not to care about using DateTime by allowing
# them to simply pass the results of calling 'time()' - It's not like it's
# still 1970...
coerce SlotOffset
from subtype(as => Int, where { $_ > TOTAL_SLOTS }
via { to_SlotOffset(DateTime->from_epoch(epoch => $_) };
}
has vector => (
is => 'ro',
isa => VectorOfWeights,
required => 1,
}
...Now, if we were programming in plain old Moose, we could rewrite weight_at like so:
sub weight_at {
my $self = shift;
my $offset = to_SlotOffset(shift);
$self->vector->[$offset]
}Which would be pretty sweet, but we’re using MooseX::Declare; there’s an even better way:
method weight_at (SlotOffset $offset does coerce) {
$self->vector->[$offset];
}Sweet!
We could stop there, but I had an insight. What we’ve got here is basically a wrapper around a delegation to our vector, and Moose’s new native types feature let us express the delegation to the vector quite neatly, like so:
has vector => (
isa => 'VectorOfWeights',
is => 'ro',
required => 1,
traits => ['Array'],
handles {
weight_at => 'get',
},
);
...
around weight_at (SlotOffset $offset does coerce) {
$self->$orig($offset);
}This could be overkill when vector is a simple ArrayRef as we have here, but the pattern of delegating declaratively in the attribute definition and then munging arguments in an around handler is applicable to more than just argument transformation. A typical delegation pattern involves having the delegating object passing itself in as an argument to the method delegated to. The nature of Moose’s handles declarations makes that impossible to do within the attribute declaration, but it’s easy to fix with an around helper:
around delegated_method (Any @args) {
$self->$orig($self, @args);
}(If you’re wrapping more than one method in this fashion, you should probably consider using a plain old Moose style around handler, which lets you wrap multiple methods with around @delegated_methods => sub {...}
So, at the end of all that, and after we’ve extracted our Type declarations into WeightVector::Types, we have:
use MooseX::Declare;
class WeightVector {
use WeightVector::Types qw(VectorOfWeights PercentageInt SlotOffset);
has vector => (
isa => 'VectorOfWeights',
is => 'ro',
required => 1,
traits => ['Array'],
handles {
weight_at => 'get',
},
);
method current_weight {
$self->weight_at(time());
}
around weight_at (SlotOffset $offset does coerce) {
$self->$orig($offset);
}
}And we’ve pushed all knowledge of DateTime off onto our type declarations and gained a boatload of handy bounds checking. We’ve also got a new tool for handling tricky delegation setups in the handles/around combo.
Notes
Motivation
I realise that this looks like a radical decomposition of the class with very little motivation, but it was driven by tests and by some other requirements that I’ve removed from the body of the post. In particular, the type coercions were driven by the need to build particular vectors for testing, a key method being:
method set_weight (PercentageInt $weight,
SlotOffset $from does coerce,
SlotOffset $to does coerce)
{
...
}Type coercion is wonderful
Generally, I’m not a fan of static typing. I’m from the “duck type all the way” school of programming,3 so most of my method declarations have no type declarations. But type declarations, especially ones that coerce, make so much sense on methods that make up the public protocol of a class. I only use type declarations on internal methods when I need a narrower coercion, or if I’m using MooseX::Multimethods, which I still haven’t used for anything but exploration.
Updates
Thanks to Chris Dolan for spotting that I’d got the SlotOffset coercion completely wrong. The real code’s doing the right thing, but that’s what comes of recreating code from memory.
1 This was actually motivated by trying to write tests to verify that the weights were correctly set.
2 I’m declaring these in a BEGIN block of the class itself mostly for explanatory purposes – there’s a good case for moving them out into a separate file and pulling it in with use.
3 Except during my periodic attempts to learn Haskell. I’ve learned Haskell at least three times now.
Test::Class::Sugar 0.3, no, 0.4
tap tap… Is this thing on?
So, I recently noticed that Test::Class 0.33 got released, which means that Test::Class::Sugar no longer needs to depend on a development release, and I also noticed that it was embarrassingly easy to throw Test::Class::Sugar into an infinite loop by forgetting which way the >> goes when you want to specify the number of subtests in a test method.
So, I’ve done a quick fix of the infinite loop problem as well and uploaded version 0.3 to PAUSE, so now you can write your tests like:
testclass exercises ClassUnderTest {
test creation of the class under test {
lives_and {
isa_ok ClassUnderTest->new, $test->subject;
}
}
...
}without having to jump through the hoops of downloading a development version of Test::Class or worry about accidental infinite loops…
Next up, fix the syntax to either allow both << and >> as test count specifiers, or come up with a more memorable way of separating the count from the test name.
Update:
Shortly after I released 0.3, Joel Bernstein asked if I’d be interested in a topic branch to make Test::Class::Sugar work with perl 5.8.
“Of course!” I said.
One day later, there it was. Thank you to Joel and to his employers, NET-A-PORTER for sponsoring his work. So now, Test::Class::Sugar 0.4 is winging its way to CPAN and now I have no excuse for not using it at work.
Perl 5, version 10.1
At last! Start your compilers everybody.
Perl: Not just a hobby any more 3
Back in January, I wrote that I was choosing Perl to revisit as my language for recreational programming.
Quite a bit has happened since then. I gave a talk on MooseX::Declare to the London Perl Mongers and will be delivering an extended version at this year’s YAPC::Europe in August.
Then, a week ago, I accepted an offer to work full time as a Perl programmer again. In London.
Sing “Ho!” for the life of the long distance commuter.
London.pm Presentation Video
Back in (crikey) February, I gave a talk at the London Perl Mongers’ technical meeting about Moose for Ruby Programmers and wrote it up here. Mike Whittaker was in the front row of the audience with his iPhone and, a couple of minutes in, started a voice recording and gave me a copy.
So… finally… I’ve taken the time I should have been using to write another article for The H and wrestled the slides and the audio into something like sync and uploaded the results to Vimeo for your viewing pleasure.
An introduction to MooseX::Declare from Piers Cawley on Vimeo.
Test::Class::Sugar released 2
I’ve just pushed the second version of Test::Class::Sugar (first discussed here). It’s pretty much as discussed in the original article, but after some discussion with David Wheeler, I’ve dropped the +uses clause from the testclass declaration in favour of less DWIMmy (and thus easier to explain behaviour).
I’ve also introduced a defaults argument at use time. The only working default in this release is one which lets you specify the prefix used to derive the test class name from the class/module under test. I’ve documented a couple of extra and so far unimplemented defaults as well.
Have a play, you might like it.
Thinking about the virtues 3
I got a bit of stick on IRC last night for some of the choices I’d made when I was writing Test::Class::Sugar, in particular because one of the prerequisites is chromatic’s handy and opinionated Modern::Perl module. The ‘controversial’ aspect of Modern::Perl is that, when you use it, your code won’t run on any Perl before Perl 5, version 10.
The thing is, I don’t care about older perls any more. Version 10 features like ~~ and // are too convenient to fart about writing circumlocutions just to run on a version of Perl that I have no intention of ever using again.
Actually, that’s not quite true, those version 10 features are too convenient for me to fart about working around their absence. If you think that a module I write is useful enough that you want it to work on version 8, then of course I’ll accept your patch. But don’t be surprised if, when I start adding new features, I break the backward compatibility.
Also, on the happy day that version 10.1 escapes the pumpking patch, I’ll be setting that as my minimum perl version even if chromatic doesn’t bump the version number in Modern::Perl.
It’s all about the virtues
Your context is different from mine. I’m writing in Perl again for my own amusement more than anything. There are developments in modern Perl – tools like Moose and Devel::Declare – that I think are exciting and important. The Announcements project I started was as much about playing with the new tools as it was about trying to write something of wider utility. Test::Class::Sugar arose as a direct result of attempting to write Announcements and the desire to write test classes without hoopage. My principle drive then, is impatience to get Test::Class::Sugar to the point where I can get back to writing Announcements.
But then laziness and hubris kick in. So the code needs some polish. The parser and the code generator need to be disentangled, I need to get Adrian Howard to apply the little patch I had to make to Test::Class. Laziness demands I document it.
Impatience tells me to lean on the features of modern perl – that way I can get back to being a user of the new library as quickly as possible. Laziness tells me that I’m not going to need backwards compatibilty. Hubris tells me my work is good enough that someone who does will like it enough to send me a patch.
Everybody wins.
Magic vs Mundane: Keeping them apart
In which your correspondent does magical battle with the guts of Perl and emerges bloodied, but unbowed with a useful principle to code by.
Skip to the conclusion if you’re uncomfortable with the guts of the Perl runtime
Test::Class had me tearing my hair out earlier. There I was, happily transforming
test something {
ok 1;
};into something very like1:
*test_something =
Sub::Name::subname(
'test_something'
=> sub : Test { ok 1 }
);through the magic of Devel::Declare, but Test::Class didn’t seem to be playing fair. Instead of letting my tests run happily, it was complaining that it:
cannot test anonymous subs – you probably loaded a Test::Class too late (after the CHECK block was run). See ‘A NOTE ON LOADING TEST CLASSES’ in perldoc Test::Class for more details
The thing is, I wasn’t loading Test::Class too late. The problem is that, at the point I applied the Test attribute to my sub, the sub didn’t have a name and, because of the constraints you’re operating under when you’re using Devel::Declare to do code transformation, there was no obvious way to give it a name in time.
Incompatible magics
The trouble is, Test::Class does what it does through the magic of compile time code attributes, and, further, it relies on the fact that if a perl subroutine that gets inserted into the symbol table like this:
sub has_a_name {...}Then, when you get hold of a reference to that code by other means (say, in the subroutine that handles the setting of an attribute, that code ref knows its own name. However, if a subroutine that ends up in the symbol table like this:
*anonymous_ref = sub {...};Doesn’t know its name, unless you take advantage of the Sub::Name module.
So, in my generated code, I was giving my coderef a name, but it was happening to late. At the point that Test::Class::Test method was seeing the coderef, the coderef was anonymous.
My magic and Test::Class’s magic were incompatible.
The thing is, both sorts of magic are really just sugar for some pretty mundane donkey work. Test::Class does what it does through attributes because no flesh and blood programmer in their right mind would want to write something like this every time they wanted to write a test method:
sub test_something {
...
}
__PACKAGE__->mark_as_special_method('test_something', 'test', '3');In fact, mark_as_special_method doesn’t even exist as its own subroutine. The code that marks a method as special is just part of the body of the Test attribute handler.
Conclusion
Which brings me neatly to my conclusion.
When you’re designing a module that does anything magical, consider starting with a mundane core API that handles the business side of things. Then layer your magic on top of that API. Then document the API and the magic. Obviously the magic bits go up front in the docs, and the API goes in its own section (or even podfile) down at the bottom, where only eejits like me, who want their magic to work slightly different to yours, will bother reading it.
Obviously, I’m motivated by an issue I’m having with a particular module from CPAN, but the principle of separating the magic and the mundane is applicable everywhere. It’s called Separation of Concerns, or The Single Responsiblity Pattern. I call it a Just Story.
You’ll find the pattern in well designed websites that are using unobtrusive javascript to wave an AJAX wand over the site. You’ll see it woven through books like The Structure and Interpretation of Computer Programmers – where it’s called an Abstraction Barrier.
Patches sent
It turns out to be very easy to add mark_as_special_method (though I actually wrote it as ‘add_testinfo’ in the patch) to Test::Class. It’s about as straightforward an Extract Method refactoring as I’ve ever done – even without automated tools, I managed not to fuck it up. There’s a patch in Adrian Howard’s inbox, and I’m hopeful that it’ll be applied soon.
1 Not exactly – that’s the result of calling the shadowed test subroutine which was the result of the code transformation.
Check out the osfameron fork of Devel::Declare for the beginnings of some decent documentation which explains what’s going on.
