The Catalyst web framework uses
Perl 5 function
attributes effectively—I've seen few more effective uses of
attributes.
Any modern web framework has to deal with the idea of routes and request
routing somehow. Given a request path (such as /stocks/AA/view_analysis),
how does your application know what to do?
Catalyst solves this elegantly with a feature known as chained actions.
Controller methods can consume zero or more parts of the path but, when
explicitly chained, can combine. Consider the example request path. The
controller is Stocks.pm. The second component of the path
(/AA) is the identifier for a stock (Alcoa, to be specific. I'm
neither long nor short on Alcoa itself, though I probably own some shares as
part of a fund somewhere.) The final component of the path,
/view_analysis, is an action—a verb representing an action the
controller should take on the object representing Alcoa in the system.
You can probably start to see the idea of the chain right away.
The Stock controller has a controller method called get_stock which
grabs the stock symbol from the request path, looks it up in the database, and
stores the object representing that stock for further processing. If no such
symbol exists, it throws an exception.
The view_analysis
method chains off of the get_stock
method such that Catalyst will only dispatch to view_analysis
when
it's already successfully dispatched to get_stock. Unless you write a
custom dispatch system which bypasses the dispatch rules, users will never be
able to call view_analysis
without a valid stock object
available.
(Further, these methods are part of a chain which requires that users have
successfully logged into the system; they chain off of a user authentication
system.)
In code terms, the relevant attributes look something like:
sub authorized :Chained('/login/required') :PathPart('stocks') :CaptureArgs(0);
sub get_stock :Chained('authorized') :PathPart('') :CaptureArgs(1);
sub view_analysis :Chained('get_stock') :PathPart('view_analysis') :Args(0);
The :Chained
attribute is most relevant here.
:PathPart
governs how Catalyst's dispatcher makes each method
visible to user requests (get_stock
doesn't consume a part of the
path on its own, while authorized
consumes the name of the
controller and view_analysis
consumes its own name).
:CaptureArgs
and :Args
control how many other pieces
of the path the methods consume; in the case of get_stock
, it's
the single path element between /stocks
and any subsequent chained
actions—in this case, /AA
. As view_analysis
is
the end point of a chain, you use :Args
instead of
:CaptureArgs
.
With that all explained, request method chaining is fantastic. I can reuse
get_stock()
for other request methods and get all of its benefits,
including the fact that only authorized users can even reach this point.
Yet I want to prove these characteristics of my application.
I want to prove these features so definitively that I don't want to write
tests for them. I want my program to fail to compile if these
characteristics are untrue.
I see chaining from get_stock()
as supplying an invariant
precondition to view_analysis()
such that it proves, to my
satisfaction, that I can always rely on a valid stock object being available
within the analysis method. Always. Similarly, I can always rely on a valid
user being available within both methods. Always always.
The problem comes in that it's easy to make a typo in the name of a chain or
a method, or to use :CaptureArgs
instead of :Args
or
vice versa.
Here's the thing: all of this metadata is metadata. All of this information
is available at compile time, before Perl has to execute anything.
If I had a really good and extensible type system in Perl 5, I could write a
couple of pieces of predicate logic to say that every chained method should be
a starting point or have a valid predecessor. These are trivial properties of
my program (no matter how large it gets) and they're resolvable with the
information available at the point of compilation. Even with complex controller
construction through the use of roles and parametric roles, this information is
available.
I know how to emulate this behavior by injecting some sort of
CHECK
block into the code and schlepping through the symbol table
and inspecting attributes myself, but that's emulating a useful feature we
could exploit in a lot of ways.
Forget the talk about making Perl into Java or C++ by adding a silly
manifest static type system. We could find and fix real errors in
logic—trivial errors, trivially discoverable—if we had an
extensible type system which let us define our own simple predicates.
(Implementing such is left as an exercise for a small army of readers cloned
from a very small army of brilliant p5p hackers with copious spare time and a
habit of reading ACM papers before breakfast.)